selection.js 71 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. ( function() {
  6. // #### checkSelectionChange : START
  7. // The selection change check basically saves the element parent tree of
  8. // the current node and check it on successive requests. If there is any
  9. // change on the tree, then the selectionChange event gets fired.
  10. function checkSelectionChange() {
  11. // A possibly available fake-selection.
  12. var sel = this._.fakeSelection,
  13. realSel;
  14. if ( sel ) {
  15. realSel = this.getSelection( 1 );
  16. // If real (not locked/stored) selection was moved from hidden container,
  17. // then the fake-selection must be invalidated.
  18. if ( !realSel || !realSel.isHidden() ) {
  19. // Remove the cache from fake-selection references in use elsewhere.
  20. sel.reset();
  21. // Have the code using the native selection.
  22. sel = 0;
  23. }
  24. }
  25. // If not fake-selection is available then get the native selection.
  26. if ( !sel ) {
  27. sel = realSel || this.getSelection( 1 );
  28. // Editor may have no selection at all.
  29. if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
  30. return;
  31. }
  32. this.fire( 'selectionCheck', sel );
  33. var currentPath = this.elementPath();
  34. if ( !currentPath.compare( this._.selectionPreviousPath ) ) {
  35. // Cache the active element, which we'll eventually lose on Webkit.
  36. if ( CKEDITOR.env.webkit )
  37. this._.previousActive = this.document.getActive();
  38. this._.selectionPreviousPath = currentPath;
  39. this.fire( 'selectionChange', { selection: sel, path: currentPath } );
  40. }
  41. }
  42. var checkSelectionChangeTimer, checkSelectionChangeTimeoutPending;
  43. function checkSelectionChangeTimeout() {
  44. // Firing the "OnSelectionChange" event on every key press started to
  45. // be too slow. This function guarantees that there will be at least
  46. // 200ms delay between selection checks.
  47. checkSelectionChangeTimeoutPending = true;
  48. if ( checkSelectionChangeTimer )
  49. return;
  50. checkSelectionChangeTimeoutExec.call( this );
  51. checkSelectionChangeTimer = CKEDITOR.tools.setTimeout( checkSelectionChangeTimeoutExec, 200, this );
  52. }
  53. function checkSelectionChangeTimeoutExec() {
  54. checkSelectionChangeTimer = null;
  55. if ( checkSelectionChangeTimeoutPending ) {
  56. // Call this with a timeout so the browser properly moves the
  57. // selection after the mouseup. It happened that the selection was
  58. // being moved after the mouseup when clicking inside selected text
  59. // with Firefox.
  60. CKEDITOR.tools.setTimeout( checkSelectionChange, 0, this );
  61. checkSelectionChangeTimeoutPending = false;
  62. }
  63. }
  64. // #### checkSelectionChange : END
  65. var isVisible = CKEDITOR.dom.walker.invisible( 1 );
  66. // May absorb the caret if:
  67. // * is a visible node,
  68. // * is a non-empty element (this rule will accept elements like <strong></strong> because they
  69. // they were not accepted by the isVisible() check, not not <br> which cannot absorb the caret).
  70. // See #12621.
  71. function mayAbsorbCaret( node ) {
  72. if ( isVisible( node ) )
  73. return true;
  74. if ( node.type == CKEDITOR.NODE_ELEMENT && !node.is( CKEDITOR.dtd.$empty ) )
  75. return true;
  76. return false;
  77. }
  78. function rangeRequiresFix( range ) {
  79. // Whether we must prevent from absorbing caret by this context node.
  80. // Also checks whether there's an editable position next to that node.
  81. function ctxRequiresFix( node, isAtEnd ) {
  82. // It's ok for us if a text node absorbs the caret, because
  83. // the caret container element isn't changed then.
  84. if ( !node || node.type == CKEDITOR.NODE_TEXT )
  85. return false;
  86. var testRng = range.clone();
  87. return testRng[ 'moveToElementEdit' + ( isAtEnd ? 'End' : 'Start' ) ]( node );
  88. }
  89. // Range root must be the editable element, it's to avoid creating filler char
  90. // on any temporary internal selection.
  91. if ( !( range.root instanceof CKEDITOR.editable ) )
  92. return false;
  93. var ct = range.startContainer;
  94. var previous = range.getPreviousNode( mayAbsorbCaret, null, ct ),
  95. next = range.getNextNode( mayAbsorbCaret, null, ct );
  96. // Any adjacent text container may absorb the caret, e.g.
  97. // <p><strong>text</strong>^foo</p>
  98. // <p>foo^<strong>text</strong></p>
  99. // <div>^<p>foo</p></div>
  100. if ( ctxRequiresFix( previous ) || ctxRequiresFix( next, 1 ) )
  101. return true;
  102. // Empty block/inline element is also affected. <span>^</span>, <p>^</p> (#7222)
  103. // If you found this line confusing check #12655.
  104. if ( !( previous || next ) && !( ct.type == CKEDITOR.NODE_ELEMENT && ct.isBlockBoundary() && ct.getBogus() ) )
  105. return true;
  106. return false;
  107. }
  108. function createFillingChar( element ) {
  109. removeFillingChar( element, false );
  110. var fillingChar = element.getDocument().createText( '\u200B' );
  111. element.setCustomData( 'cke-fillingChar', fillingChar );
  112. return fillingChar;
  113. }
  114. function getFillingChar( element ) {
  115. return element.getCustomData( 'cke-fillingChar' );
  116. }
  117. // Checks if a filling char has been used, eventualy removing it (#1272).
  118. function checkFillingChar( element ) {
  119. var fillingChar = getFillingChar( element );
  120. if ( fillingChar ) {
  121. // Use this flag to avoid removing the filling char right after
  122. // creating it.
  123. if ( fillingChar.getCustomData( 'ready' ) )
  124. removeFillingChar( element );
  125. else
  126. fillingChar.setCustomData( 'ready', 1 );
  127. }
  128. }
  129. function removeFillingChar( element, keepSelection ) {
  130. var fillingChar = element && element.removeCustomData( 'cke-fillingChar' );
  131. if ( fillingChar ) {
  132. // Text selection position might get mangled by
  133. // subsequent dom modification, save it now for restoring. (#8617)
  134. if ( keepSelection !== false ) {
  135. var bm,
  136. sel = element.getDocument().getSelection().getNative(),
  137. // Be error proof.
  138. range = sel && sel.type != 'None' && sel.getRangeAt( 0 );
  139. if ( fillingChar.getLength() > 1 && range && range.intersectsNode( fillingChar.$ ) ) {
  140. bm = createNativeSelectionBookmark( sel );
  141. // Anticipate the offset change brought by the removed char.
  142. var startAffected = sel.anchorNode == fillingChar.$ && sel.anchorOffset > 0,
  143. endAffected = sel.focusNode == fillingChar.$ && sel.focusOffset > 0;
  144. startAffected && bm[ 0 ].offset--;
  145. endAffected && bm[ 1 ].offset--;
  146. }
  147. }
  148. // We can't simply remove the filling node because the user
  149. // will actually enlarge it when typing, so we just remove the
  150. // invisible char from it.
  151. fillingChar.setText( replaceFillingChar( fillingChar.getText() ) );
  152. // Restore the bookmark preserving selection's direction.
  153. if ( bm ) {
  154. moveNativeSelectionToBookmark( element.getDocument().$, bm );
  155. }
  156. }
  157. }
  158. function replaceFillingChar( html ) {
  159. return html.replace( /\u200B( )?/g, function( match ) {
  160. // #10291 if filling char is followed by a space replace it with nbsp.
  161. return match[ 1 ] ? '\xa0' : '';
  162. } );
  163. }
  164. function createNativeSelectionBookmark( sel ) {
  165. return [
  166. { node: sel.anchorNode, offset: sel.anchorOffset },
  167. { node: sel.focusNode, offset: sel.focusOffset }
  168. ];
  169. }
  170. function moveNativeSelectionToBookmark( document, bm ) {
  171. var sel = document.getSelection(),
  172. range = document.createRange();
  173. range.setStart( bm[ 0 ].node, bm[ 0 ].offset );
  174. range.collapse( true );
  175. sel.removeAllRanges();
  176. sel.addRange( range );
  177. sel.extend( bm[ 1 ].node, bm[ 1 ].offset );
  178. }
  179. // Creates cke_hidden_sel container and puts real selection there.
  180. function hideSelection( editor ) {
  181. var style = CKEDITOR.env.ie ? 'display:none' : 'position:fixed;top:0;left:-1000px',
  182. hiddenEl = CKEDITOR.dom.element.createFromHtml(
  183. '<div data-cke-hidden-sel="1" data-cke-temp="1" style="' + style + '">&nbsp;</div>',
  184. editor.document );
  185. editor.fire( 'lockSnapshot' );
  186. editor.editable().append( hiddenEl );
  187. // Always use real selection to avoid overriding locked one (http://dev.ckeditor.com/ticket/11104#comment:13).
  188. var sel = editor.getSelection( 1 ),
  189. range = editor.createRange(),
  190. // Cancel selectionchange fired by selectRanges - prevent from firing selectionChange.
  191. listener = sel.root.on( 'selectionchange', function( evt ) {
  192. evt.cancel();
  193. }, null, null, 0 );
  194. range.setStartAt( hiddenEl, CKEDITOR.POSITION_AFTER_START );
  195. range.setEndAt( hiddenEl, CKEDITOR.POSITION_BEFORE_END );
  196. sel.selectRanges( [ range ] );
  197. listener.removeListener();
  198. editor.fire( 'unlockSnapshot' );
  199. // Set this value at the end, so reset() executed by selectRanges()
  200. // will clean up old hidden selection container.
  201. editor._.hiddenSelectionContainer = hiddenEl;
  202. }
  203. function removeHiddenSelectionContainer( editor ) {
  204. var hiddenEl = editor._.hiddenSelectionContainer;
  205. if ( hiddenEl ) {
  206. var isDirty = editor.checkDirty();
  207. editor.fire( 'lockSnapshot' );
  208. hiddenEl.remove();
  209. editor.fire( 'unlockSnapshot' );
  210. !isDirty && editor.resetDirty();
  211. }
  212. delete editor._.hiddenSelectionContainer;
  213. }
  214. // Object containing keystroke handlers for fake selection.
  215. var fakeSelectionDefaultKeystrokeHandlers = ( function() {
  216. function leave( right ) {
  217. return function( evt ) {
  218. var range = evt.editor.createRange();
  219. // Move selection only if there's a editable place for it.
  220. // It no, then do nothing (keystroke will be blocked, widget selection kept).
  221. if ( range.moveToClosestEditablePosition( evt.selected, right ) )
  222. evt.editor.getSelection().selectRanges( [ range ] );
  223. // Prevent default.
  224. return false;
  225. };
  226. }
  227. function del( right ) {
  228. return function( evt ) {
  229. var editor = evt.editor,
  230. range = editor.createRange(),
  231. found;
  232. // If haven't found place for caret on the default side,
  233. // try to find it on the other side.
  234. if ( !( found = range.moveToClosestEditablePosition( evt.selected, right ) ) )
  235. found = range.moveToClosestEditablePosition( evt.selected, !right );
  236. if ( found )
  237. editor.getSelection().selectRanges( [ range ] );
  238. // Save the state before removing selected element.
  239. editor.fire( 'saveSnapshot' );
  240. evt.selected.remove();
  241. // Haven't found any editable space before removing element,
  242. // try to place the caret anywhere (most likely, in empty editable).
  243. if ( !found ) {
  244. range.moveToElementEditablePosition( editor.editable() );
  245. editor.getSelection().selectRanges( [ range ] );
  246. }
  247. editor.fire( 'saveSnapshot' );
  248. // Prevent default.
  249. return false;
  250. };
  251. }
  252. var leaveLeft = leave(),
  253. leaveRight = leave( 1 );
  254. return {
  255. 37: leaveLeft, // LEFT
  256. 38: leaveLeft, // UP
  257. 39: leaveRight, // RIGHT
  258. 40: leaveRight, // DOWN
  259. 8: del(), // BACKSPACE
  260. 46: del( 1 ) // DELETE
  261. };
  262. } )();
  263. // Handle left, right, delete and backspace keystrokes next to non-editable elements
  264. // by faking selection on them.
  265. function getOnKeyDownListener( editor ) {
  266. var keystrokes = { 37: 1, 39: 1, 8: 1, 46: 1 };
  267. return function( evt ) {
  268. var keystroke = evt.data.getKeystroke();
  269. // Handle only left/right/del/bspace keys.
  270. if ( !keystrokes[ keystroke ] )
  271. return;
  272. var sel = editor.getSelection(),
  273. ranges = sel.getRanges(),
  274. range = ranges[ 0 ];
  275. // Handle only single range and it has to be collapsed.
  276. if ( ranges.length != 1 || !range.collapsed )
  277. return;
  278. var next = range[ keystroke < 38 ? 'getPreviousEditableNode' : 'getNextEditableNode' ]();
  279. if ( next && next.type == CKEDITOR.NODE_ELEMENT && next.getAttribute( 'contenteditable' ) == 'false' ) {
  280. editor.getSelection().fake( next );
  281. evt.data.preventDefault();
  282. evt.cancel();
  283. }
  284. };
  285. }
  286. // If fake selection should be applied this function will return instance of
  287. // CKEDITOR.dom.element which should gain fake selection.
  288. function getNonEditableFakeSelectionReceiver( ranges ) {
  289. var enclosedNode, shrinkedNode, clone, range;
  290. if ( ranges.length == 1 && !( range = ranges[ 0 ] ).collapsed &&
  291. ( enclosedNode = range.getEnclosedNode() ) && enclosedNode.type == CKEDITOR.NODE_ELEMENT ) {
  292. // So far we can't say that enclosed element is non-editable. Before checking,
  293. // we'll shrink range (clone). Shrinking will stop on non-editable range, or
  294. // innermost element (#11114).
  295. clone = range.clone();
  296. clone.shrink( CKEDITOR.SHRINK_ELEMENT, true );
  297. // If shrinked range still encloses an element, check this one (shrink stops only on non-editable elements).
  298. if ( ( shrinkedNode = clone.getEnclosedNode() ) && shrinkedNode.type == CKEDITOR.NODE_ELEMENT )
  299. enclosedNode = shrinkedNode;
  300. if ( enclosedNode.getAttribute( 'contenteditable' ) == 'false' )
  301. return enclosedNode;
  302. }
  303. }
  304. // Fix ranges which may end after hidden selection container.
  305. // Note: this function may only be used if hidden selection container
  306. // is not in DOM any more.
  307. function fixRangesAfterHiddenSelectionContainer( ranges, root ) {
  308. var range;
  309. for ( var i = 0; i < ranges.length; ++i ) {
  310. range = ranges[ i ];
  311. if ( range.endContainer.equals( root ) ) {
  312. // We can use getChildCount() because hidden selection container is not in DOM.
  313. range.endOffset = Math.min( range.endOffset, root.getChildCount() );
  314. }
  315. }
  316. }
  317. // Extract only editable part or ranges.
  318. // Note: this function modifies ranges list!
  319. // @param {CKEDITOR.dom.rangeList} ranges
  320. function extractEditableRanges( ranges ) {
  321. for ( var i = 0; i < ranges.length; i++ ) {
  322. var range = ranges[ i ];
  323. // Drop range spans inside one ready-only node.
  324. var parent = range.getCommonAncestor();
  325. if ( parent.isReadOnly() )
  326. ranges.splice( i, 1 );
  327. if ( range.collapsed )
  328. continue;
  329. // Range may start inside a non-editable element,
  330. // replace the range start after it.
  331. if ( range.startContainer.isReadOnly() ) {
  332. var current = range.startContainer,
  333. isElement;
  334. while ( current ) {
  335. isElement = current.type == CKEDITOR.NODE_ELEMENT;
  336. if ( ( isElement && current.is( 'body' ) ) || !current.isReadOnly() )
  337. break;
  338. if ( isElement && current.getAttribute( 'contentEditable' ) == 'false' )
  339. range.setStartAfter( current );
  340. current = current.getParent();
  341. }
  342. }
  343. var startContainer = range.startContainer,
  344. endContainer = range.endContainer,
  345. startOffset = range.startOffset,
  346. endOffset = range.endOffset,
  347. walkerRange = range.clone();
  348. // Enlarge range start/end with text node to avoid walker
  349. // being DOM destructive, it doesn't interfere our checking
  350. // of elements below as well.
  351. if ( startContainer && startContainer.type == CKEDITOR.NODE_TEXT ) {
  352. if ( startOffset >= startContainer.getLength() )
  353. walkerRange.setStartAfter( startContainer );
  354. else
  355. walkerRange.setStartBefore( startContainer );
  356. }
  357. if ( endContainer && endContainer.type == CKEDITOR.NODE_TEXT ) {
  358. if ( !endOffset )
  359. walkerRange.setEndBefore( endContainer );
  360. else
  361. walkerRange.setEndAfter( endContainer );
  362. }
  363. // Looking for non-editable element inside the range.
  364. var walker = new CKEDITOR.dom.walker( walkerRange );
  365. walker.evaluator = function( node ) {
  366. if ( node.type == CKEDITOR.NODE_ELEMENT && node.isReadOnly() ) {
  367. var newRange = range.clone();
  368. range.setEndBefore( node );
  369. // Drop collapsed range around read-only elements,
  370. // it make sure the range list empty when selecting
  371. // only non-editable elements.
  372. if ( range.collapsed )
  373. ranges.splice( i--, 1 );
  374. // Avoid creating invalid range.
  375. if ( !( node.getPosition( walkerRange.endContainer ) & CKEDITOR.POSITION_CONTAINS ) ) {
  376. newRange.setStartAfter( node );
  377. if ( !newRange.collapsed )
  378. ranges.splice( i + 1, 0, newRange );
  379. }
  380. return true;
  381. }
  382. return false;
  383. };
  384. walker.next();
  385. }
  386. return ranges;
  387. }
  388. // Setup all editor instances for the necessary selection hooks.
  389. CKEDITOR.on( 'instanceCreated', function( ev ) {
  390. var editor = ev.editor;
  391. editor.on( 'contentDom', function() {
  392. var doc = editor.document,
  393. outerDoc = CKEDITOR.document,
  394. editable = editor.editable(),
  395. body = doc.getBody(),
  396. html = doc.getDocumentElement();
  397. var isInline = editable.isInline();
  398. var restoreSel,
  399. lastSel;
  400. // Give the editable an initial selection on first focus,
  401. // put selection at a consistent position at the start
  402. // of the contents. (#9507)
  403. if ( CKEDITOR.env.gecko ) {
  404. editable.attachListener( editable, 'focus', function( evt ) {
  405. evt.removeListener();
  406. if ( restoreSel !== 0 ) {
  407. var nativ = editor.getSelection().getNative();
  408. // Do it only if the native selection is at an unwanted
  409. // place (at the very start of the editable). #10119
  410. if ( nativ && nativ.isCollapsed && nativ.anchorNode == editable.$ ) {
  411. var rng = editor.createRange();
  412. rng.moveToElementEditStart( editable );
  413. rng.select();
  414. }
  415. }
  416. }, null, null, -2 );
  417. }
  418. // Plays the magic here to restore/save dom selection on editable focus/blur.
  419. editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
  420. // On Webkit we use DOMFocusIn which is fired more often than focus - e.g. when moving from main editable
  421. // to nested editable (or the opposite). Unlock selection all, but restore only when it was locked
  422. // for the same active element, what will e.g. mean restoring after displaying dialog.
  423. if ( restoreSel && CKEDITOR.env.webkit )
  424. restoreSel = editor._.previousActive && editor._.previousActive.equals( doc.getActive() );
  425. editor.unlockSelection( restoreSel );
  426. restoreSel = 0;
  427. }, null, null, -1 );
  428. // Disable selection restoring when clicking in.
  429. editable.attachListener( editable, 'mousedown', function() {
  430. restoreSel = 0;
  431. } );
  432. // Save a cloned version of current selection.
  433. function saveSel() {
  434. lastSel = new CKEDITOR.dom.selection( editor.getSelection() );
  435. lastSel.lock();
  436. }
  437. // Browsers could loose the selection once the editable lost focus,
  438. // in such case we need to reproduce it by saving a locked selection
  439. // and restoring it upon focus gain.
  440. if ( CKEDITOR.env.ie || isInline ) {
  441. // For old IEs, we can retrieve the last correct DOM selection upon the "beforedeactivate" event.
  442. // For the rest, a more frequent check is required for each selection change made.
  443. if ( isMSSelection )
  444. editable.attachListener( editable, 'beforedeactivate', saveSel, null, null, -1 );
  445. else
  446. editable.attachListener( editor, 'selectionCheck', saveSel, null, null, -1 );
  447. // Lock the selection and mark it to be restored.
  448. // On Webkit we use DOMFocusOut which is fired more often than blur. I.e. it will also be
  449. // fired when nested editable is blurred.
  450. editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusOut' : 'blur', function() {
  451. editor.lockSelection( lastSel );
  452. restoreSel = 1;
  453. }, null, null, -1 );
  454. // Disable selection restoring when clicking in.
  455. editable.attachListener( editable, 'mousedown', function() {
  456. restoreSel = 0;
  457. } );
  458. }
  459. // The following selection-related fixes only apply to classic (`iframe`-based) editable.
  460. if ( CKEDITOR.env.ie && !isInline ) {
  461. var scroll;
  462. editable.attachListener( editable, 'mousedown', function( evt ) {
  463. // IE scrolls document to top on right mousedown
  464. // when editor has no focus, remember this scroll
  465. // position and revert it before context menu opens. (#5778)
  466. if ( evt.data.$.button == 2 ) {
  467. var sel = editor.document.getSelection();
  468. if ( !sel || sel.getType() == CKEDITOR.SELECTION_NONE )
  469. scroll = editor.window.getScrollPosition();
  470. }
  471. } );
  472. editable.attachListener( editable, 'mouseup', function( evt ) {
  473. // Restore recorded scroll position when needed on right mouseup.
  474. if ( evt.data.$.button == 2 && scroll ) {
  475. editor.document.$.documentElement.scrollLeft = scroll.x;
  476. editor.document.$.documentElement.scrollTop = scroll.y;
  477. }
  478. scroll = null;
  479. } );
  480. // When content doc is in standards mode, IE doesn't focus the editor when
  481. // clicking at the region below body (on html element) content, we emulate
  482. // the normal behavior on old IEs. (#1659, #7932)
  483. if ( doc.$.compatMode != 'BackCompat' ) {
  484. if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
  485. html.on( 'mousedown', function( evt ) {
  486. evt = evt.data;
  487. // Expand the text range along with mouse move.
  488. function onHover( evt ) {
  489. evt = evt.data.$;
  490. if ( textRng ) {
  491. // Read the current cursor.
  492. var rngEnd = body.$.createTextRange();
  493. moveRangeToPoint( rngEnd, evt.clientX, evt.clientY );
  494. // Handle drag directions.
  495. textRng.setEndPoint(
  496. startRng.compareEndPoints( 'StartToStart', rngEnd ) < 0 ?
  497. 'EndToEnd' : 'StartToStart', rngEnd );
  498. // Update selection with new range.
  499. textRng.select();
  500. }
  501. }
  502. function removeListeners() {
  503. outerDoc.removeListener( 'mouseup', onSelectEnd );
  504. html.removeListener( 'mouseup', onSelectEnd );
  505. }
  506. function onSelectEnd() {
  507. html.removeListener( 'mousemove', onHover );
  508. removeListeners();
  509. // Make it in effect on mouse up. (#9022)
  510. textRng.select();
  511. }
  512. // We're sure that the click happens at the region
  513. // below body, but not on scrollbar.
  514. if ( evt.getTarget().is( 'html' ) &&
  515. evt.$.y < html.$.clientHeight &&
  516. evt.$.x < html.$.clientWidth ) {
  517. // Start to build the text range.
  518. var textRng = body.$.createTextRange();
  519. moveRangeToPoint( textRng, evt.$.clientX, evt.$.clientY );
  520. // Records the dragging start of the above text range.
  521. var startRng = textRng.duplicate();
  522. html.on( 'mousemove', onHover );
  523. outerDoc.on( 'mouseup', onSelectEnd );
  524. html.on( 'mouseup', onSelectEnd );
  525. }
  526. } );
  527. }
  528. // It's much simpler for IE8+, we just need to reselect the reported range.
  529. // This hack does not work on IE>=11 because there's no old selection&range APIs.
  530. if ( CKEDITOR.env.version > 7 && CKEDITOR.env.version < 11 ) {
  531. html.on( 'mousedown', function( evt ) {
  532. if ( evt.data.getTarget().is( 'html' ) ) {
  533. // Limit the text selection mouse move inside of editable. (#9715)
  534. outerDoc.on( 'mouseup', onSelectEnd );
  535. html.on( 'mouseup', onSelectEnd );
  536. }
  537. } );
  538. }
  539. }
  540. }
  541. // We check the selection change:
  542. // 1. Upon "selectionchange" event from the editable element. (which might be faked event fired by our code)
  543. // 2. After the accomplish of keyboard and mouse events.
  544. editable.attachListener( editable, 'selectionchange', checkSelectionChange, editor );
  545. editable.attachListener( editable, 'keyup', checkSelectionChangeTimeout, editor );
  546. // Always fire the selection change on focus gain.
  547. // On Webkit do this on DOMFocusIn, because the selection is unlocked on it too and
  548. // we need synchronization between those listeners to not lost cached editor._.previousActive property
  549. // (which is updated on selectionCheck).
  550. editable.attachListener( editable, CKEDITOR.env.webkit ? 'DOMFocusIn' : 'focus', function() {
  551. editor.forceNextSelectionCheck();
  552. editor.selectionChange( 1 );
  553. } );
  554. // #9699: On Webkit&Gecko in inline editor we have to check selection when it was changed
  555. // by dragging and releasing mouse button outside editable. Dragging (mousedown)
  556. // has to be initialized in editable, but for mouseup we listen on document element.
  557. if ( isInline && ( CKEDITOR.env.webkit || CKEDITOR.env.gecko ) ) {
  558. var mouseDown;
  559. editable.attachListener( editable, 'mousedown', function() {
  560. mouseDown = 1;
  561. } );
  562. editable.attachListener( doc.getDocumentElement(), 'mouseup', function() {
  563. if ( mouseDown )
  564. checkSelectionChangeTimeout.call( editor );
  565. mouseDown = 0;
  566. } );
  567. }
  568. // In all other cases listen on simple mouseup over editable, as we did before #9699.
  569. //
  570. // Use document instead of editable in non-IEs for observing mouseup
  571. // since editable won't fire the event if selection process started within iframe and ended out
  572. // of the editor (#9851).
  573. else {
  574. editable.attachListener( CKEDITOR.env.ie ? editable : doc.getDocumentElement(), 'mouseup', checkSelectionChangeTimeout, editor );
  575. }
  576. if ( CKEDITOR.env.webkit ) {
  577. // Before keystroke is handled by editor, check to remove the filling char.
  578. editable.attachListener( doc, 'keydown', function( evt ) {
  579. var key = evt.data.getKey();
  580. // Remove the filling char before some keys get
  581. // executed, so they'll not get blocked by it.
  582. switch ( key ) {
  583. case 13: // ENTER
  584. case 33: // PAGEUP
  585. case 34: // PAGEDOWN
  586. case 35: // HOME
  587. case 36: // END
  588. case 37: // LEFT-ARROW
  589. case 39: // RIGHT-ARROW
  590. case 8: // BACKSPACE
  591. case 45: // INS
  592. case 46: // DEl
  593. removeFillingChar( editable );
  594. }
  595. }, null, null, -1 );
  596. }
  597. // Automatically select non-editable element when navigating into
  598. // it by left/right or backspace/del keys.
  599. editable.attachListener( editable, 'keydown', getOnKeyDownListener( editor ), null, null, -1 );
  600. function moveRangeToPoint( range, x, y ) {
  601. // Error prune in IE7. (#9034, #9110)
  602. try {
  603. range.moveToPoint( x, y );
  604. } catch ( e ) {}
  605. }
  606. function removeListeners() {
  607. outerDoc.removeListener( 'mouseup', onSelectEnd );
  608. html.removeListener( 'mouseup', onSelectEnd );
  609. }
  610. function onSelectEnd() {
  611. removeListeners();
  612. // The event is not fired when clicking on the scrollbars,
  613. // so we can safely check the following to understand
  614. // whether the empty space following <body> has been clicked.
  615. var sel = CKEDITOR.document.$.selection,
  616. range = sel.createRange();
  617. // The selection range is reported on host, but actually it should applies to the content doc.
  618. if ( sel.type != 'None' && range.parentElement().ownerDocument == doc.$ )
  619. range.select();
  620. }
  621. } );
  622. editor.on( 'setData', function() {
  623. // Invalidate locked selection when unloading DOM.
  624. // (#9521, #5217#comment:32 and #11500#comment:11)
  625. editor.unlockSelection();
  626. // Webkit's selection will mess up after the data loading.
  627. if ( CKEDITOR.env.webkit )
  628. clearSelection();
  629. } );
  630. // Catch all the cases which above setData listener couldn't catch.
  631. // For example: switching to source mode and destroying editor.
  632. editor.on( 'contentDomUnload', function() {
  633. editor.unlockSelection();
  634. } );
  635. // IE9 might cease to work if there's an object selection inside the iframe (#7639).
  636. if ( CKEDITOR.env.ie9Compat )
  637. editor.on( 'beforeDestroy', clearSelection, null, null, 9 );
  638. // Check selection change on data reload.
  639. editor.on( 'dataReady', function() {
  640. // Clean up fake selection after setting data.
  641. delete editor._.fakeSelection;
  642. delete editor._.hiddenSelectionContainer;
  643. editor.selectionChange( 1 );
  644. } );
  645. // When loaded data are ready check whether hidden selection container was not loaded.
  646. editor.on( 'loadSnapshot', function() {
  647. var isElement = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_ELEMENT ),
  648. // TODO replace with el.find() which will be introduced in #9764,
  649. // because it may happen that hidden sel container won't be the last element.
  650. last = editor.editable().getLast( isElement );
  651. if ( last && last.hasAttribute( 'data-cke-hidden-sel' ) ) {
  652. last.remove();
  653. // Firefox does a very unfortunate thing. When a non-editable element is the only
  654. // element in the editable, when we remove the hidden selection container, Firefox
  655. // will insert a bogus <br> at the beginning of the editable...
  656. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=911201
  657. //
  658. // This behavior is never desired because this <br> pushes the content lower, but in
  659. // this case it is especially dangerous, because it happens when a bookmark is being restored.
  660. // Since this <br> is inserted at the beginning it changes indexes and thus breaks the bookmark2
  661. // what results in errors.
  662. //
  663. // So... let's revert what Firefox broke.
  664. if ( CKEDITOR.env.gecko ) {
  665. var first = editor.editable().getFirst( isElement );
  666. if ( first && first.is( 'br' ) && first.getAttribute( '_moz_editor_bogus_node' ) ) {
  667. first.remove();
  668. }
  669. }
  670. }
  671. }, null, null, 100 );
  672. editor.on( 'key', function( evt ) {
  673. if ( editor.mode != 'wysiwyg' )
  674. return;
  675. var sel = editor.getSelection();
  676. if ( !sel.isFake )
  677. return;
  678. var handler = fakeSelectionDefaultKeystrokeHandlers[ evt.data.keyCode ];
  679. if ( handler )
  680. return handler( { editor: editor, selected: sel.getSelectedElement(), selection: sel, keyEvent: evt } );
  681. } );
  682. function clearSelection() {
  683. var sel = editor.getSelection();
  684. sel && sel.removeAllRanges();
  685. }
  686. } );
  687. CKEDITOR.on( 'instanceReady', function( evt ) {
  688. var editor = evt.editor,
  689. fillingCharBefore,
  690. selectionBookmark;
  691. // On WebKit only, we need a special "filling" char on some situations
  692. // (#1272). Here we set the events that should invalidate that char.
  693. if ( CKEDITOR.env.webkit ) {
  694. editor.on( 'selectionChange', function() {
  695. checkFillingChar( editor.editable() );
  696. }, null, null, -1 );
  697. editor.on( 'beforeSetMode', function() {
  698. removeFillingChar( editor.editable() );
  699. }, null, null, -1 );
  700. editor.on( 'beforeUndoImage', beforeData );
  701. editor.on( 'afterUndoImage', afterData );
  702. editor.on( 'beforeGetData', beforeData, null, null, 0 );
  703. editor.on( 'getData', afterData );
  704. }
  705. function beforeData() {
  706. var editable = editor.editable();
  707. if ( !editable )
  708. return;
  709. var fillingChar = getFillingChar( editable );
  710. if ( fillingChar ) {
  711. // If the selection's focus or anchor is located in the filling char's text node,
  712. // we need to restore the selection in afterData, because it will be lost
  713. // when setting text. Selection's direction must be preserved.
  714. // (#7437, #12489, #12491 comment:3)
  715. var sel = editor.document.$.getSelection();
  716. if ( sel.type != 'None' && ( sel.anchorNode == fillingChar.$ || sel.focusNode == fillingChar.$ ) )
  717. selectionBookmark = createNativeSelectionBookmark( sel );
  718. fillingCharBefore = fillingChar.getText();
  719. fillingChar.setText( replaceFillingChar( fillingCharBefore ) );
  720. }
  721. }
  722. function afterData() {
  723. var editable = editor.editable();
  724. if ( !editable )
  725. return;
  726. var fillingChar = getFillingChar( editable );
  727. if ( fillingChar ) {
  728. fillingChar.setText( fillingCharBefore );
  729. if ( selectionBookmark ) {
  730. moveNativeSelectionToBookmark( editor.document.$, selectionBookmark );
  731. selectionBookmark = null;
  732. }
  733. }
  734. }
  735. } );
  736. /**
  737. * Check the selection change in editor and potentially fires
  738. * the {@link CKEDITOR.editor#event-selectionChange} event.
  739. *
  740. * @method
  741. * @member CKEDITOR.editor
  742. * @param {Boolean} [checkNow=false] Force the check to happen immediately
  743. * instead of coming with a timeout delay (default).
  744. */
  745. CKEDITOR.editor.prototype.selectionChange = function( checkNow ) {
  746. ( checkNow ? checkSelectionChange : checkSelectionChangeTimeout ).call( this );
  747. };
  748. /**
  749. * Retrieve the editor selection in scope of editable element.
  750. *
  751. * **Note:** Since the native browser selection provides only one single
  752. * selection at a time per document, so if editor's editable element has lost focus,
  753. * this method will return a null value unless the {@link CKEDITOR.editor#lockSelection}
  754. * has been called beforehand so the saved selection is retrieved.
  755. *
  756. * var selection = CKEDITOR.instances.editor1.getSelection();
  757. * alert( selection.getType() );
  758. *
  759. * @method
  760. * @member CKEDITOR.editor
  761. * @param {Boolean} forceRealSelection Return real selection, instead of saved or fake one.
  762. * @returns {CKEDITOR.dom.selection} A selection object or null if not available for the moment.
  763. */
  764. CKEDITOR.editor.prototype.getSelection = function( forceRealSelection ) {
  765. // Check if there exists a locked or fake selection.
  766. if ( ( this._.savedSelection || this._.fakeSelection ) && !forceRealSelection )
  767. return this._.savedSelection || this._.fakeSelection;
  768. // Editable element might be absent or editor might not be in a wysiwyg mode.
  769. var editable = this.editable();
  770. return editable && this.mode == 'wysiwyg' ? new CKEDITOR.dom.selection( editable ) : null;
  771. };
  772. /**
  773. * Locks the selection made in the editor in order to make it possible to
  774. * manipulate it without browser interference. A locked selection is
  775. * cached and remains unchanged until it is released with the
  776. * {@link CKEDITOR.editor#unlockSelection} method.
  777. *
  778. * @method
  779. * @member CKEDITOR.editor
  780. * @param {CKEDITOR.dom.selection} [sel] Specify the selection to be locked.
  781. * @returns {Boolean} `true` if selection was locked.
  782. */
  783. CKEDITOR.editor.prototype.lockSelection = function( sel ) {
  784. sel = sel || this.getSelection( 1 );
  785. if ( sel.getType() != CKEDITOR.SELECTION_NONE ) {
  786. !sel.isLocked && sel.lock();
  787. this._.savedSelection = sel;
  788. return true;
  789. }
  790. return false;
  791. };
  792. /**
  793. * Unlocks the selection made in the editor and locked with the
  794. * {@link CKEDITOR.editor#unlockSelection} method. An unlocked selection
  795. * is no longer cached and can be changed.
  796. *
  797. * @method
  798. * @member CKEDITOR.editor
  799. * @param {Boolean} [restore] If set to `true`, the selection is
  800. * restored back to the selection saved earlier by using the
  801. * {@link CKEDITOR.dom.selection#lock} method.
  802. */
  803. CKEDITOR.editor.prototype.unlockSelection = function( restore ) {
  804. var sel = this._.savedSelection;
  805. if ( sel ) {
  806. sel.unlock( restore );
  807. delete this._.savedSelection;
  808. return true;
  809. }
  810. return false;
  811. };
  812. /**
  813. * @method
  814. * @member CKEDITOR.editor
  815. * @todo
  816. */
  817. CKEDITOR.editor.prototype.forceNextSelectionCheck = function() {
  818. delete this._.selectionPreviousPath;
  819. };
  820. /**
  821. * Gets the current selection in context of the document's body element.
  822. *
  823. * var selection = CKEDITOR.instances.editor1.document.getSelection();
  824. * alert( selection.getType() );
  825. *
  826. * @method
  827. * @member CKEDITOR.dom.document
  828. * @returns {CKEDITOR.dom.selection} A selection object.
  829. */
  830. CKEDITOR.dom.document.prototype.getSelection = function() {
  831. return new CKEDITOR.dom.selection( this );
  832. };
  833. /**
  834. * Select this range as the only one with {@link CKEDITOR.dom.selection#selectRanges}.
  835. *
  836. * @method
  837. * @returns {CKEDITOR.dom.selection}
  838. * @member CKEDITOR.dom.range
  839. */
  840. CKEDITOR.dom.range.prototype.select = function() {
  841. var sel = this.root instanceof CKEDITOR.editable ? this.root.editor.getSelection() : new CKEDITOR.dom.selection( this.root );
  842. sel.selectRanges( [ this ] );
  843. return sel;
  844. };
  845. /**
  846. * No selection.
  847. *
  848. * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_NONE )
  849. * alert( 'Nothing is selected' );
  850. *
  851. * @readonly
  852. * @property {Number} [=1]
  853. * @member CKEDITOR
  854. */
  855. CKEDITOR.SELECTION_NONE = 1;
  856. /**
  857. * A text or a collapsed selection.
  858. *
  859. * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
  860. * alert( 'A text is selected' );
  861. *
  862. * @readonly
  863. * @property {Number} [=2]
  864. * @member CKEDITOR
  865. */
  866. CKEDITOR.SELECTION_TEXT = 2;
  867. /**
  868. * Element selection.
  869. *
  870. * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_ELEMENT )
  871. * alert( 'An element is selected' );
  872. *
  873. * @readonly
  874. * @property {Number} [=3]
  875. * @member CKEDITOR
  876. */
  877. CKEDITOR.SELECTION_ELEMENT = 3;
  878. var isMSSelection = typeof window.getSelection != 'function',
  879. nextRev = 1;
  880. /**
  881. * Manipulates the selection within a DOM element. If the current browser selection
  882. * spans outside of the element, an empty selection object is returned.
  883. *
  884. * Despite the fact that selection's constructor allows to create selection instances,
  885. * usually it's better to get selection from the editor instance:
  886. *
  887. * var sel = editor.getSelection();
  888. *
  889. * See {@link CKEDITOR.editor#getSelection}.
  890. *
  891. * @class
  892. * @constructor Creates a selection class instance.
  893. *
  894. * // Selection scoped in document.
  895. * var sel = new CKEDITOR.dom.selection( CKEDITOR.document );
  896. *
  897. * // Selection scoped in element with 'editable' id.
  898. * var sel = new CKEDITOR.dom.selection( CKEDITOR.document.getById( 'editable' ) );
  899. *
  900. * // Cloning selection.
  901. * var clone = new CKEDITOR.dom.selection( sel );
  902. *
  903. * @param {CKEDITOR.dom.document/CKEDITOR.dom.element/CKEDITOR.dom.selection} target
  904. * The DOM document/element that the DOM selection is restrained to. Only selection which spans
  905. * within the target element is considered as valid.
  906. *
  907. * If {@link CKEDITOR.dom.selection} is passed, then its clone will be created.
  908. */
  909. CKEDITOR.dom.selection = function( target ) {
  910. // Target is a selection - clone it.
  911. if ( target instanceof CKEDITOR.dom.selection ) {
  912. var selection = target;
  913. target = target.root;
  914. }
  915. var isElement = target instanceof CKEDITOR.dom.element,
  916. root;
  917. this.rev = selection ? selection.rev : nextRev++;
  918. this.document = target instanceof CKEDITOR.dom.document ? target : target.getDocument();
  919. this.root = root = isElement ? target : this.document.getBody();
  920. this.isLocked = 0;
  921. this._ = {
  922. cache: {}
  923. };
  924. // Clone selection.
  925. if ( selection ) {
  926. CKEDITOR.tools.extend( this._.cache, selection._.cache );
  927. this.isFake = selection.isFake;
  928. this.isLocked = selection.isLocked;
  929. return this;
  930. }
  931. // Check whether browser focus is really inside of the editable element.
  932. var nativeSel = this.getNative(),
  933. rangeParent,
  934. range;
  935. if ( nativeSel ) {
  936. if ( nativeSel.getRangeAt ) {
  937. range = nativeSel.rangeCount && nativeSel.getRangeAt( 0 );
  938. rangeParent = range && new CKEDITOR.dom.node( range.commonAncestorContainer );
  939. }
  940. // For old IEs.
  941. else {
  942. // Sometimes, mostly when selection is close to the table or hr,
  943. // IE throws "Unspecified error".
  944. try {
  945. range = nativeSel.createRange();
  946. } catch ( err ) {}
  947. rangeParent = range && CKEDITOR.dom.element.get( range.item && range.item( 0 ) || range.parentElement() );
  948. }
  949. }
  950. // Selection out of concerned range, empty the selection.
  951. // TODO check whether this condition cannot be reverted to its old
  952. // form (commented out) after we closed #10438.
  953. //if ( !( rangeParent && ( root.equals( rangeParent ) || root.contains( rangeParent ) ) ) ) {
  954. if ( !(
  955. rangeParent &&
  956. ( rangeParent.type == CKEDITOR.NODE_ELEMENT || rangeParent.type == CKEDITOR.NODE_TEXT ) &&
  957. ( this.root.equals( rangeParent ) || this.root.contains( rangeParent ) )
  958. ) ) {
  959. this._.cache.type = CKEDITOR.SELECTION_NONE;
  960. this._.cache.startElement = null;
  961. this._.cache.selectedElement = null;
  962. this._.cache.selectedText = '';
  963. this._.cache.ranges = new CKEDITOR.dom.rangeList();
  964. }
  965. return this;
  966. };
  967. var styleObjectElements = { img: 1, hr: 1, li: 1, table: 1, tr: 1, td: 1, th: 1, embed: 1, object: 1, ol: 1, ul: 1,
  968. a: 1, input: 1, form: 1, select: 1, textarea: 1, button: 1, fieldset: 1, thead: 1, tfoot: 1 };
  969. CKEDITOR.dom.selection.prototype = {
  970. /**
  971. * Gets the native selection object from the browser.
  972. *
  973. * var selection = editor.getSelection().getNative();
  974. *
  975. * @returns {Object} The native browser selection object.
  976. */
  977. getNative: function() {
  978. if ( this._.cache.nativeSel !== undefined )
  979. return this._.cache.nativeSel;
  980. return ( this._.cache.nativeSel = isMSSelection ? this.document.$.selection : this.document.getWindow().$.getSelection() );
  981. },
  982. /**
  983. * Gets the type of the current selection. The following values are
  984. * available:
  985. *
  986. * * {@link CKEDITOR#SELECTION_NONE} (1): No selection.
  987. * * {@link CKEDITOR#SELECTION_TEXT} (2): A text or a collapsed selection is selected.
  988. * * {@link CKEDITOR#SELECTION_ELEMENT} (3): An element is selected.
  989. *
  990. * Example:
  991. *
  992. * if ( editor.getSelection().getType() == CKEDITOR.SELECTION_TEXT )
  993. * alert( 'A text is selected' );
  994. *
  995. * @method
  996. * @returns {Number} One of the following constant values: {@link CKEDITOR#SELECTION_NONE},
  997. * {@link CKEDITOR#SELECTION_TEXT} or {@link CKEDITOR#SELECTION_ELEMENT}.
  998. */
  999. getType: isMSSelection ?
  1000. function() {
  1001. var cache = this._.cache;
  1002. if ( cache.type )
  1003. return cache.type;
  1004. var type = CKEDITOR.SELECTION_NONE;
  1005. try {
  1006. var sel = this.getNative(),
  1007. ieType = sel.type;
  1008. if ( ieType == 'Text' )
  1009. type = CKEDITOR.SELECTION_TEXT;
  1010. if ( ieType == 'Control' )
  1011. type = CKEDITOR.SELECTION_ELEMENT;
  1012. // It is possible that we can still get a text range
  1013. // object even when type == 'None' is returned by IE.
  1014. // So we'd better check the object returned by
  1015. // createRange() rather than by looking at the type.
  1016. if ( sel.createRange().parentElement() )
  1017. type = CKEDITOR.SELECTION_TEXT;
  1018. } catch ( e ) {}
  1019. return ( cache.type = type );
  1020. } : function() {
  1021. var cache = this._.cache;
  1022. if ( cache.type )
  1023. return cache.type;
  1024. var type = CKEDITOR.SELECTION_TEXT;
  1025. var sel = this.getNative();
  1026. if ( !( sel && sel.rangeCount ) )
  1027. type = CKEDITOR.SELECTION_NONE;
  1028. else if ( sel.rangeCount == 1 ) {
  1029. // Check if the actual selection is a control (IMG,
  1030. // TABLE, HR, etc...).
  1031. var range = sel.getRangeAt( 0 ),
  1032. startContainer = range.startContainer;
  1033. if ( startContainer == range.endContainer && startContainer.nodeType == 1 &&
  1034. ( range.endOffset - range.startOffset ) == 1 &&
  1035. styleObjectElements[ startContainer.childNodes[ range.startOffset ].nodeName.toLowerCase() ] ) {
  1036. type = CKEDITOR.SELECTION_ELEMENT;
  1037. }
  1038. }
  1039. return ( cache.type = type );
  1040. },
  1041. /**
  1042. * Retrieves the {@link CKEDITOR.dom.range} instances that represent the current selection.
  1043. *
  1044. * Note: Some browsers return multiple ranges even for a continuous selection. Firefox, for example, returns
  1045. * one range for each table cell when one or more table rows are selected.
  1046. *
  1047. * var ranges = selection.getRanges();
  1048. * alert( ranges.length );
  1049. *
  1050. * @method
  1051. * @param {Boolean} [onlyEditables] If set to `true`, this function retrives editable ranges only.
  1052. * @returns {Array} Range instances that represent the current selection.
  1053. */
  1054. getRanges: ( function() {
  1055. var func = isMSSelection ? ( function() {
  1056. function getNodeIndex( node ) {
  1057. return new CKEDITOR.dom.node( node ).getIndex();
  1058. }
  1059. // Finds the container and offset for a specific boundary
  1060. // of an IE range.
  1061. var getBoundaryInformation = function( range, start ) {
  1062. // Creates a collapsed range at the requested boundary.
  1063. range = range.duplicate();
  1064. range.collapse( start );
  1065. // Gets the element that encloses the range entirely.
  1066. var parent = range.parentElement();
  1067. // Empty parent element, e.g. <i>^</i>
  1068. if ( !parent.hasChildNodes() )
  1069. return { container: parent, offset: 0 };
  1070. var siblings = parent.children,
  1071. child, sibling,
  1072. testRange = range.duplicate(),
  1073. startIndex = 0,
  1074. endIndex = siblings.length - 1,
  1075. index = -1,
  1076. position, distance, container;
  1077. // Binary search over all element childs to test the range to see whether
  1078. // range is right on the boundary of one element.
  1079. while ( startIndex <= endIndex ) {
  1080. index = Math.floor( ( startIndex + endIndex ) / 2 );
  1081. child = siblings[ index ];
  1082. testRange.moveToElementText( child );
  1083. position = testRange.compareEndPoints( 'StartToStart', range );
  1084. if ( position > 0 )
  1085. endIndex = index - 1;
  1086. else if ( position < 0 )
  1087. startIndex = index + 1;
  1088. else
  1089. return { container: parent, offset: getNodeIndex( child ) };
  1090. }
  1091. // All childs are text nodes,
  1092. // or to the right hand of test range are all text nodes. (#6992)
  1093. if ( index == -1 || index == siblings.length - 1 && position < 0 ) {
  1094. // Adapt test range to embrace the entire parent contents.
  1095. testRange.moveToElementText( parent );
  1096. testRange.setEndPoint( 'StartToStart', range );
  1097. // IE report line break as CRLF with range.text but
  1098. // only LF with textnode.nodeValue, normalize them to avoid
  1099. // breaking character counting logic below. (#3949)
  1100. distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
  1101. siblings = parent.childNodes;
  1102. // Actual range anchor right beside test range at the boundary of text node.
  1103. if ( !distance ) {
  1104. child = siblings[ siblings.length - 1 ];
  1105. if ( child.nodeType != CKEDITOR.NODE_TEXT )
  1106. return { container: parent, offset: siblings.length };
  1107. else
  1108. return { container: child, offset: child.nodeValue.length };
  1109. }
  1110. // Start the measuring until distance overflows, meanwhile count the text nodes.
  1111. var i = siblings.length;
  1112. while ( distance > 0 && i > 0 ) {
  1113. sibling = siblings[ --i ];
  1114. if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
  1115. container = sibling;
  1116. distance -= sibling.nodeValue.length;
  1117. }
  1118. }
  1119. return { container: container, offset: -distance };
  1120. }
  1121. // Test range was one offset beyond OR behind the anchored text node.
  1122. else {
  1123. // Adapt one side of test range to the actual range
  1124. // for measuring the offset between them.
  1125. testRange.collapse( position > 0 ? true : false );
  1126. testRange.setEndPoint( position > 0 ? 'StartToStart' : 'EndToStart', range );
  1127. // IE report line break as CRLF with range.text but
  1128. // only LF with textnode.nodeValue, normalize them to avoid
  1129. // breaking character counting logic below. (#3949)
  1130. distance = testRange.text.replace( /(\r\n|\r)/g, '\n' ).length;
  1131. // Actual range anchor right beside test range at the inner boundary of text node.
  1132. if ( !distance )
  1133. return { container: parent, offset: getNodeIndex( child ) + ( position > 0 ? 0 : 1 ) };
  1134. // Start the measuring until distance overflows, meanwhile count the text nodes.
  1135. while ( distance > 0 ) {
  1136. try {
  1137. sibling = child[ position > 0 ? 'previousSibling' : 'nextSibling' ];
  1138. if ( sibling.nodeType == CKEDITOR.NODE_TEXT ) {
  1139. distance -= sibling.nodeValue.length;
  1140. container = sibling;
  1141. }
  1142. child = sibling;
  1143. }
  1144. // Measurement in IE could be somtimes wrong because of <select> element. (#4611)
  1145. catch ( e ) {
  1146. return { container: parent, offset: getNodeIndex( child ) };
  1147. }
  1148. }
  1149. return { container: container, offset: position > 0 ? -distance : container.nodeValue.length + distance };
  1150. }
  1151. };
  1152. return function() {
  1153. // IE doesn't have range support (in the W3C way), so we
  1154. // need to do some magic to transform selections into
  1155. // CKEDITOR.dom.range instances.
  1156. var sel = this.getNative(),
  1157. nativeRange = sel && sel.createRange(),
  1158. type = this.getType(),
  1159. range;
  1160. if ( !sel )
  1161. return [];
  1162. if ( type == CKEDITOR.SELECTION_TEXT ) {
  1163. range = new CKEDITOR.dom.range( this.root );
  1164. var boundaryInfo = getBoundaryInformation( nativeRange, true );
  1165. range.setStart( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
  1166. boundaryInfo = getBoundaryInformation( nativeRange );
  1167. range.setEnd( new CKEDITOR.dom.node( boundaryInfo.container ), boundaryInfo.offset );
  1168. // Correct an invalid IE range case on empty list item. (#5850)
  1169. if ( range.endContainer.getPosition( range.startContainer ) & CKEDITOR.POSITION_PRECEDING && range.endOffset <= range.startContainer.getIndex() )
  1170. range.collapse();
  1171. return [ range ];
  1172. } else if ( type == CKEDITOR.SELECTION_ELEMENT ) {
  1173. var retval = [];
  1174. for ( var i = 0; i < nativeRange.length; i++ ) {
  1175. var element = nativeRange.item( i ),
  1176. parentElement = element.parentNode,
  1177. j = 0;
  1178. range = new CKEDITOR.dom.range( this.root );
  1179. for ( ; j < parentElement.childNodes.length && parentElement.childNodes[ j ] != element; j++ ) {
  1180. }
  1181. range.setStart( new CKEDITOR.dom.node( parentElement ), j );
  1182. range.setEnd( new CKEDITOR.dom.node( parentElement ), j + 1 );
  1183. retval.push( range );
  1184. }
  1185. return retval;
  1186. }
  1187. return [];
  1188. };
  1189. } )() :
  1190. function() {
  1191. // On browsers implementing the W3C range, we simply
  1192. // tranform the native ranges in CKEDITOR.dom.range
  1193. // instances.
  1194. var ranges = [],
  1195. range,
  1196. sel = this.getNative();
  1197. if ( !sel )
  1198. return ranges;
  1199. for ( var i = 0; i < sel.rangeCount; i++ ) {
  1200. var nativeRange = sel.getRangeAt( i );
  1201. range = new CKEDITOR.dom.range( this.root );
  1202. range.setStart( new CKEDITOR.dom.node( nativeRange.startContainer ), nativeRange.startOffset );
  1203. range.setEnd( new CKEDITOR.dom.node( nativeRange.endContainer ), nativeRange.endOffset );
  1204. ranges.push( range );
  1205. }
  1206. return ranges;
  1207. };
  1208. return function( onlyEditables ) {
  1209. var cache = this._.cache,
  1210. ranges = cache.ranges;
  1211. if ( !ranges )
  1212. cache.ranges = ranges = new CKEDITOR.dom.rangeList( func.call( this ) );
  1213. if ( !onlyEditables )
  1214. return ranges;
  1215. // Split range into multiple by read-only nodes.
  1216. // Clone ranges array to avoid changing cached ranges (#11493).
  1217. return extractEditableRanges( new CKEDITOR.dom.rangeList( ranges.slice() ) );
  1218. };
  1219. } )(),
  1220. /**
  1221. * Gets the DOM element in which the selection starts.
  1222. *
  1223. * var element = editor.getSelection().getStartElement();
  1224. * alert( element.getName() );
  1225. *
  1226. * @returns {CKEDITOR.dom.element} The element at the beginning of the selection.
  1227. */
  1228. getStartElement: function() {
  1229. var cache = this._.cache;
  1230. if ( cache.startElement !== undefined )
  1231. return cache.startElement;
  1232. var node;
  1233. switch ( this.getType() ) {
  1234. case CKEDITOR.SELECTION_ELEMENT:
  1235. return this.getSelectedElement();
  1236. case CKEDITOR.SELECTION_TEXT:
  1237. var range = this.getRanges()[ 0 ];
  1238. if ( range ) {
  1239. if ( !range.collapsed ) {
  1240. range.optimize();
  1241. // Decrease the range content to exclude particial
  1242. // selected node on the start which doesn't have
  1243. // visual impact. ( #3231 )
  1244. while ( 1 ) {
  1245. var startContainer = range.startContainer,
  1246. startOffset = range.startOffset;
  1247. // Limit the fix only to non-block elements.(#3950)
  1248. if ( startOffset == ( startContainer.getChildCount ? startContainer.getChildCount() : startContainer.getLength() ) && !startContainer.isBlockBoundary() )
  1249. range.setStartAfter( startContainer );
  1250. else
  1251. break;
  1252. }
  1253. node = range.startContainer;
  1254. if ( node.type != CKEDITOR.NODE_ELEMENT )
  1255. return node.getParent();
  1256. node = node.getChild( range.startOffset );
  1257. if ( !node || node.type != CKEDITOR.NODE_ELEMENT )
  1258. node = range.startContainer;
  1259. else {
  1260. var child = node.getFirst();
  1261. while ( child && child.type == CKEDITOR.NODE_ELEMENT ) {
  1262. node = child;
  1263. child = child.getFirst();
  1264. }
  1265. }
  1266. } else {
  1267. node = range.startContainer;
  1268. if ( node.type != CKEDITOR.NODE_ELEMENT )
  1269. node = node.getParent();
  1270. }
  1271. node = node.$;
  1272. }
  1273. }
  1274. return cache.startElement = ( node ? new CKEDITOR.dom.element( node ) : null );
  1275. },
  1276. /**
  1277. * Gets the currently selected element.
  1278. *
  1279. * var element = editor.getSelection().getSelectedElement();
  1280. * alert( element.getName() );
  1281. *
  1282. * @returns {CKEDITOR.dom.element} The selected element. Null if no
  1283. * selection is available or the selection type is not {@link CKEDITOR#SELECTION_ELEMENT}.
  1284. */
  1285. getSelectedElement: function() {
  1286. var cache = this._.cache;
  1287. if ( cache.selectedElement !== undefined )
  1288. return cache.selectedElement;
  1289. var self = this;
  1290. var node = CKEDITOR.tools.tryThese(
  1291. // Is it native IE control type selection?
  1292. function() {
  1293. return self.getNative().createRange().item( 0 );
  1294. },
  1295. // Figure it out by checking if there's a single enclosed
  1296. // node of the range.
  1297. function() {
  1298. var range = self.getRanges()[ 0 ].clone(),
  1299. enclosed, selected;
  1300. // Check first any enclosed element, e.g. <ul>[<li><a href="#">item</a></li>]</ul>
  1301. for ( var i = 2; i && !( ( enclosed = range.getEnclosedNode() ) && ( enclosed.type == CKEDITOR.NODE_ELEMENT ) && styleObjectElements[ enclosed.getName() ] && ( selected = enclosed ) ); i-- ) {
  1302. // Then check any deep wrapped element, e.g. [<b><i><img /></i></b>]
  1303. range.shrink( CKEDITOR.SHRINK_ELEMENT );
  1304. }
  1305. return selected && selected.$;
  1306. }
  1307. );
  1308. return cache.selectedElement = ( node ? new CKEDITOR.dom.element( node ) : null );
  1309. },
  1310. /**
  1311. * Retrieves the text contained within the range. An empty string is returned for non-text selection.
  1312. *
  1313. * var text = editor.getSelection().getSelectedText();
  1314. * alert( text );
  1315. *
  1316. * @since 3.6.1
  1317. * @returns {String} A string of text within the current selection.
  1318. */
  1319. getSelectedText: function() {
  1320. var cache = this._.cache;
  1321. if ( cache.selectedText !== undefined )
  1322. return cache.selectedText;
  1323. var nativeSel = this.getNative(),
  1324. text = isMSSelection ? nativeSel.type == 'Control' ? '' : nativeSel.createRange().text : nativeSel.toString();
  1325. return ( cache.selectedText = text );
  1326. },
  1327. /**
  1328. * Locks the selection made in the editor in order to make it possible to
  1329. * manipulate it without browser interference. A locked selection is
  1330. * cached and remains unchanged until it is released with the {@link #unlock} method.
  1331. *
  1332. * editor.getSelection().lock();
  1333. */
  1334. lock: function() {
  1335. // Call all cacheable function.
  1336. this.getRanges();
  1337. this.getStartElement();
  1338. this.getSelectedElement();
  1339. this.getSelectedText();
  1340. // The native selection is not available when locked.
  1341. this._.cache.nativeSel = null;
  1342. this.isLocked = 1;
  1343. },
  1344. /**
  1345. * @todo
  1346. */
  1347. unlock: function( restore ) {
  1348. if ( !this.isLocked )
  1349. return;
  1350. if ( restore ) {
  1351. var selectedElement = this.getSelectedElement(),
  1352. ranges = !selectedElement && this.getRanges(),
  1353. faked = this.isFake;
  1354. }
  1355. this.isLocked = 0;
  1356. this.reset();
  1357. if ( restore ) {
  1358. // Saved selection may be outdated (e.g. anchored in offline nodes).
  1359. // Avoid getting broken by such.
  1360. var common = selectedElement || ranges[ 0 ] && ranges[ 0 ].getCommonAncestor();
  1361. if ( !( common && common.getAscendant( 'body', 1 ) ) )
  1362. return;
  1363. if ( faked )
  1364. this.fake( selectedElement );
  1365. else if ( selectedElement )
  1366. this.selectElement( selectedElement );
  1367. else
  1368. this.selectRanges( ranges );
  1369. }
  1370. },
  1371. /**
  1372. * Clears the selection cache.
  1373. *
  1374. * editor.getSelection().reset();
  1375. */
  1376. reset: function() {
  1377. this._.cache = {};
  1378. this.isFake = 0;
  1379. var editor = this.root.editor;
  1380. // Invalidate any fake selection available in the editor.
  1381. if ( editor && editor._.fakeSelection ) {
  1382. // Test whether this selection is the one that was
  1383. // faked or its clone.
  1384. if ( this.rev == editor._.fakeSelection.rev ) {
  1385. delete editor._.fakeSelection;
  1386. removeHiddenSelectionContainer( editor );
  1387. }
  1388. // jshint ignore:start
  1389. else { // %REMOVE_LINE%
  1390. window.console && console.log( '[CKEDITOR.dom.selection.reset] Wrong selection instance resets fake selection.' ); // %REMOVE_LINE%
  1391. } // %REMOVE_LINE%
  1392. // jshint ignore:end
  1393. }
  1394. this.rev = nextRev++;
  1395. },
  1396. /**
  1397. * Makes the current selection of type {@link CKEDITOR#SELECTION_ELEMENT} by enclosing the specified element.
  1398. *
  1399. * var element = editor.document.getById( 'sampleElement' );
  1400. * editor.getSelection().selectElement( element );
  1401. *
  1402. * @param {CKEDITOR.dom.element} element The element to enclose in the selection.
  1403. */
  1404. selectElement: function( element ) {
  1405. var range = new CKEDITOR.dom.range( this.root );
  1406. range.setStartBefore( element );
  1407. range.setEndAfter( element );
  1408. this.selectRanges( [ range ] );
  1409. },
  1410. /**
  1411. * Clears the original selection and adds the specified ranges to the document selection.
  1412. *
  1413. * // Move selection to the end of the editable element.
  1414. * var range = editor.createRange();
  1415. * range.moveToPosition( range.root, CKEDITOR.POSITION_BEFORE_END );
  1416. * editor.getSelection().selectRanges( [ ranges ] );
  1417. *
  1418. * @param {Array} ranges An array of {@link CKEDITOR.dom.range} instances
  1419. * representing ranges to be added to the document.
  1420. */
  1421. selectRanges: function( ranges ) {
  1422. var editor = this.root.editor,
  1423. hadHiddenSelectionContainer = editor && editor._.hiddenSelectionContainer;
  1424. this.reset();
  1425. // Check if there's a hiddenSelectionContainer in editable at some index.
  1426. // Some ranges may be anchored after the hiddenSelectionContainer and,
  1427. // once the container is removed while resetting the selection, they
  1428. // may need new endOffset (one element less within the range) (#11021 #11393).
  1429. if ( hadHiddenSelectionContainer )
  1430. fixRangesAfterHiddenSelectionContainer( ranges, this.root );
  1431. if ( !ranges.length )
  1432. return;
  1433. // Refresh the locked selection.
  1434. if ( this.isLocked ) {
  1435. // making a new DOM selection will force the focus on editable in certain situation,
  1436. // we have to save the currently focused element for later recovery.
  1437. var focused = CKEDITOR.document.getActive();
  1438. this.unlock();
  1439. this.selectRanges( ranges );
  1440. this.lock();
  1441. // Return to the previously focused element.
  1442. focused && !focused.equals( this.root ) && focused.focus();
  1443. return;
  1444. }
  1445. // Handle special case - automatic fake selection on non-editable elements.
  1446. var receiver = getNonEditableFakeSelectionReceiver( ranges );
  1447. if ( receiver ) {
  1448. this.fake( receiver );
  1449. return;
  1450. }
  1451. if ( isMSSelection ) {
  1452. var notWhitespaces = CKEDITOR.dom.walker.whitespaces( true ),
  1453. fillerTextRegex = /\ufeff|\u00a0/,
  1454. nonCells = { table: 1, tbody: 1, tr: 1 };
  1455. if ( ranges.length > 1 ) {
  1456. // IE doesn't accept multiple ranges selection, so we join all into one.
  1457. var last = ranges[ ranges.length - 1 ];
  1458. ranges[ 0 ].setEnd( last.endContainer, last.endOffset );
  1459. }
  1460. var range = ranges[ 0 ];
  1461. var collapsed = range.collapsed,
  1462. isStartMarkerAlone, dummySpan, ieRange;
  1463. // Try to make a object selection, be careful with selecting phase element in IE
  1464. // will breaks the selection in non-framed environment.
  1465. var selected = range.getEnclosedNode();
  1466. if ( selected && selected.type == CKEDITOR.NODE_ELEMENT && selected.getName() in styleObjectElements &&
  1467. !( selected.is( 'a' ) && selected.getText() ) ) {
  1468. try {
  1469. ieRange = selected.$.createControlRange();
  1470. ieRange.addElement( selected.$ );
  1471. ieRange.select();
  1472. return;
  1473. } catch ( er ) {}
  1474. }
  1475. // IE doesn't support selecting the entire table row/cell, move the selection into cells, e.g.
  1476. // <table><tbody><tr>[<td>cell</b></td>... => <table><tbody><tr><td>[cell</td>...
  1477. if ( range.startContainer.type == CKEDITOR.NODE_ELEMENT && range.startContainer.getName() in nonCells ||
  1478. range.endContainer.type == CKEDITOR.NODE_ELEMENT && range.endContainer.getName() in nonCells ) {
  1479. range.shrink( CKEDITOR.NODE_ELEMENT, true );
  1480. // The range might get collapsed (#7975). Update cached variable.
  1481. collapsed = range.collapsed;
  1482. }
  1483. var bookmark = range.createBookmark();
  1484. // Create marker tags for the start and end boundaries.
  1485. var startNode = bookmark.startNode;
  1486. var endNode;
  1487. if ( !collapsed )
  1488. endNode = bookmark.endNode;
  1489. // Create the main range which will be used for the selection.
  1490. ieRange = range.document.$.body.createTextRange();
  1491. // Position the range at the start boundary.
  1492. ieRange.moveToElementText( startNode.$ );
  1493. ieRange.moveStart( 'character', 1 );
  1494. if ( endNode ) {
  1495. // Create a tool range for the end.
  1496. var ieRangeEnd = range.document.$.body.createTextRange();
  1497. // Position the tool range at the end.
  1498. ieRangeEnd.moveToElementText( endNode.$ );
  1499. // Move the end boundary of the main range to match the tool range.
  1500. ieRange.setEndPoint( 'EndToEnd', ieRangeEnd );
  1501. ieRange.moveEnd( 'character', -1 );
  1502. } else {
  1503. // The isStartMarkerAlone logic comes from V2. It guarantees that the lines
  1504. // will expand and that the cursor will be blinking on the right place.
  1505. // Actually, we are using this flag just to avoid using this hack in all
  1506. // situations, but just on those needed.
  1507. var next = startNode.getNext( notWhitespaces );
  1508. var inPre = startNode.hasAscendant( 'pre' );
  1509. isStartMarkerAlone = ( !( next && next.getText && next.getText().match( fillerTextRegex ) ) && // already a filler there?
  1510. ( inPre || !startNode.hasPrevious() || ( startNode.getPrevious().is && startNode.getPrevious().is( 'br' ) ) ) );
  1511. // Append a temporary <span>&#65279;</span> before the selection.
  1512. // This is needed to avoid IE destroying selections inside empty
  1513. // inline elements, like <b></b> (#253).
  1514. // It is also needed when placing the selection right after an inline
  1515. // element to avoid the selection moving inside of it.
  1516. dummySpan = range.document.createElement( 'span' );
  1517. dummySpan.setHtml( '&#65279;' ); // Zero Width No-Break Space (U+FEFF). See #1359.
  1518. dummySpan.insertBefore( startNode );
  1519. if ( isStartMarkerAlone ) {
  1520. // To expand empty blocks or line spaces after <br>, we need
  1521. // instead to have any char, which will be later deleted using the
  1522. // selection.
  1523. // \ufeff = Zero Width No-Break Space (U+FEFF). (#1359)
  1524. range.document.createText( '\ufeff' ).insertBefore( startNode );
  1525. }
  1526. }
  1527. // Remove the markers (reset the position, because of the changes in the DOM tree).
  1528. range.setStartBefore( startNode );
  1529. startNode.remove();
  1530. if ( collapsed ) {
  1531. if ( isStartMarkerAlone ) {
  1532. // Move the selection start to include the temporary \ufeff.
  1533. ieRange.moveStart( 'character', -1 );
  1534. ieRange.select();
  1535. // Remove our temporary stuff.
  1536. range.document.$.selection.clear();
  1537. } else {
  1538. ieRange.select();
  1539. }
  1540. range.moveToPosition( dummySpan, CKEDITOR.POSITION_BEFORE_START );
  1541. dummySpan.remove();
  1542. } else {
  1543. range.setEndBefore( endNode );
  1544. endNode.remove();
  1545. ieRange.select();
  1546. }
  1547. } else {
  1548. var sel = this.getNative();
  1549. // getNative() returns null if iframe is "display:none" in FF. (#6577)
  1550. if ( !sel )
  1551. return;
  1552. this.removeAllRanges();
  1553. for ( var i = 0; i < ranges.length; i++ ) {
  1554. // Joining sequential ranges introduced by
  1555. // readonly elements protection.
  1556. if ( i < ranges.length - 1 ) {
  1557. var left = ranges[ i ],
  1558. right = ranges[ i + 1 ],
  1559. between = left.clone();
  1560. between.setStart( left.endContainer, left.endOffset );
  1561. between.setEnd( right.startContainer, right.startOffset );
  1562. // Don't confused by Firefox adjancent multi-ranges
  1563. // introduced by table cells selection.
  1564. if ( !between.collapsed ) {
  1565. between.shrink( CKEDITOR.NODE_ELEMENT, true );
  1566. var ancestor = between.getCommonAncestor(),
  1567. enclosed = between.getEnclosedNode();
  1568. // The following cases has to be considered:
  1569. // 1. <span contenteditable="false">[placeholder]</span>
  1570. // 2. <input contenteditable="false" type="radio"/> (#6621)
  1571. if ( ancestor.isReadOnly() || enclosed && enclosed.isReadOnly() ) {
  1572. right.setStart( left.startContainer, left.startOffset );
  1573. ranges.splice( i--, 1 );
  1574. continue;
  1575. }
  1576. }
  1577. }
  1578. range = ranges[ i ];
  1579. var nativeRange = this.document.$.createRange();
  1580. if ( range.collapsed && CKEDITOR.env.webkit && rangeRequiresFix( range ) ) {
  1581. // Append a zero-width space so WebKit will not try to
  1582. // move the selection by itself (#1272).
  1583. var fillingChar = createFillingChar( this.root );
  1584. range.insertNode( fillingChar );
  1585. next = fillingChar.getNext();
  1586. // If the filling char is followed by a <br>, whithout
  1587. // having something before it, it'll not blink.
  1588. // Let's remove it in this case.
  1589. if ( next && !fillingChar.getPrevious() && next.type == CKEDITOR.NODE_ELEMENT && next.getName() == 'br' ) {
  1590. removeFillingChar( this.root );
  1591. range.moveToPosition( next, CKEDITOR.POSITION_BEFORE_START );
  1592. } else {
  1593. range.moveToPosition( fillingChar, CKEDITOR.POSITION_AFTER_END );
  1594. }
  1595. }
  1596. nativeRange.setStart( range.startContainer.$, range.startOffset );
  1597. try {
  1598. nativeRange.setEnd( range.endContainer.$, range.endOffset );
  1599. } catch ( e ) {
  1600. // There is a bug in Firefox implementation (it would be too easy
  1601. // otherwise). The new start can't be after the end (W3C says it can).
  1602. // So, let's create a new range and collapse it to the desired point.
  1603. if ( e.toString().indexOf( 'NS_ERROR_ILLEGAL_VALUE' ) >= 0 ) {
  1604. range.collapse( 1 );
  1605. nativeRange.setEnd( range.endContainer.$, range.endOffset );
  1606. } else {
  1607. throw e;
  1608. }
  1609. }
  1610. // Select the range.
  1611. sel.addRange( nativeRange );
  1612. }
  1613. }
  1614. this.reset();
  1615. // Fakes the IE DOM event "selectionchange" on editable.
  1616. this.root.fire( 'selectionchange' );
  1617. },
  1618. /**
  1619. * Makes a "fake selection" of an element.
  1620. *
  1621. * A fake selection does not render UI artifacts over the selected
  1622. * element. Additionally, the browser native selection system is not
  1623. * aware of the fake selection. In practice, the native selection is
  1624. * moved to a hidden place where no native selection UI artifacts are
  1625. * displayed to the user.
  1626. *
  1627. * @param {CKEDITOR.dom.element} element The element to be "selected".
  1628. */
  1629. fake: function( element ) {
  1630. var editor = this.root.editor;
  1631. // Cleanup after previous selection - e.g. remove hidden sel container.
  1632. this.reset();
  1633. hideSelection( editor );
  1634. // Set this value after executing hiseSelection, because it may
  1635. // cause reset() which overwrites cache.
  1636. var cache = this._.cache;
  1637. // Caches a range than holds the element.
  1638. var range = new CKEDITOR.dom.range( this.root );
  1639. range.setStartBefore( element );
  1640. range.setEndAfter( element );
  1641. cache.ranges = new CKEDITOR.dom.rangeList( range );
  1642. // Put this element in the cache.
  1643. cache.selectedElement = cache.startElement = element;
  1644. cache.type = CKEDITOR.SELECTION_ELEMENT;
  1645. // Properties that will not be available when isFake.
  1646. cache.selectedText = cache.nativeSel = null;
  1647. this.isFake = 1;
  1648. this.rev = nextRev++;
  1649. // Save this selection, so it can be returned by editor.getSelection().
  1650. editor._.fakeSelection = this;
  1651. // Fire selectionchange, just like a normal selection.
  1652. this.root.fire( 'selectionchange' );
  1653. },
  1654. /**
  1655. * Checks whether selection is placed in hidden element.
  1656. *
  1657. * This method is to be used to verify whether fake selection
  1658. * (see {@link #fake}) is still hidden.
  1659. *
  1660. * **Note:** this method should be executed on real selection - e.g.:
  1661. *
  1662. * editor.getSelection( true ).isHidden();
  1663. *
  1664. * @returns {Boolean}
  1665. */
  1666. isHidden: function() {
  1667. var el = this.getCommonAncestor();
  1668. if ( el && el.type == CKEDITOR.NODE_TEXT )
  1669. el = el.getParent();
  1670. return !!( el && el.data( 'cke-hidden-sel' ) );
  1671. },
  1672. /**
  1673. * Creates a bookmark for each range of this selection (from {@link #getRanges})
  1674. * by calling the {@link CKEDITOR.dom.range#createBookmark} method,
  1675. * with extra care taken to avoid interference among those ranges. The arguments
  1676. * received are the same as with the underlying range method.
  1677. *
  1678. * var bookmarks = editor.getSelection().createBookmarks();
  1679. *
  1680. * @returns {Array} Array of bookmarks for each range.
  1681. */
  1682. createBookmarks: function( serializable ) {
  1683. var bookmark = this.getRanges().createBookmarks( serializable );
  1684. this.isFake && ( bookmark.isFake = 1 );
  1685. return bookmark;
  1686. },
  1687. /**
  1688. * Creates a bookmark for each range of this selection (from {@link #getRanges})
  1689. * by calling the {@link CKEDITOR.dom.range#createBookmark2} method,
  1690. * with extra care taken to avoid interference among those ranges. The arguments
  1691. * received are the same as with the underlying range method.
  1692. *
  1693. * var bookmarks = editor.getSelection().createBookmarks2();
  1694. *
  1695. * @returns {Array} Array of bookmarks for each range.
  1696. */
  1697. createBookmarks2: function( normalized ) {
  1698. var bookmark = this.getRanges().createBookmarks2( normalized );
  1699. this.isFake && ( bookmark.isFake = 1 );
  1700. return bookmark;
  1701. },
  1702. /**
  1703. * Selects the virtual ranges denoted by the bookmarks by calling {@link #selectRanges}.
  1704. *
  1705. * var bookmarks = editor.getSelection().createBookmarks();
  1706. * editor.getSelection().selectBookmarks( bookmarks );
  1707. *
  1708. * @param {Array} bookmarks The bookmarks representing ranges to be selected.
  1709. * @returns {CKEDITOR.dom.selection} This selection object, after the ranges were selected.
  1710. */
  1711. selectBookmarks: function( bookmarks ) {
  1712. var ranges = [],
  1713. node;
  1714. for ( var i = 0; i < bookmarks.length; i++ ) {
  1715. var range = new CKEDITOR.dom.range( this.root );
  1716. range.moveToBookmark( bookmarks[ i ] );
  1717. ranges.push( range );
  1718. }
  1719. // It may happen that the content change during loading, before selection is set so bookmark leads to text node.
  1720. if ( bookmarks.isFake ) {
  1721. node = ranges[ 0 ].getEnclosedNode();
  1722. if ( !node || node.type != CKEDITOR.NODE_ELEMENT ) {
  1723. // %REMOVE_START%
  1724. window.console && console.log( '[CKEDITOR.dom.selection.selectBookmarks] Selection is no longer fake.' ); // jshint ignore:line
  1725. // %REMOVE_END%
  1726. bookmarks.isFake = 0;
  1727. }
  1728. }
  1729. if ( bookmarks.isFake )
  1730. this.fake( node );
  1731. else
  1732. this.selectRanges( ranges );
  1733. return this;
  1734. },
  1735. /**
  1736. * Retrieves the common ancestor node of the first range and the last range.
  1737. *
  1738. * var ancestor = editor.getSelection().getCommonAncestor();
  1739. *
  1740. * @returns {CKEDITOR.dom.element} The common ancestor of the selection or `null` if selection is empty.
  1741. */
  1742. getCommonAncestor: function() {
  1743. var ranges = this.getRanges();
  1744. if ( !ranges.length )
  1745. return null;
  1746. var startNode = ranges[ 0 ].startContainer,
  1747. endNode = ranges[ ranges.length - 1 ].endContainer;
  1748. return startNode.getCommonAncestor( endNode );
  1749. },
  1750. /**
  1751. * Moves the scrollbar to the starting position of the current selection.
  1752. *
  1753. * editor.getSelection().scrollIntoView();
  1754. */
  1755. scrollIntoView: function() {
  1756. // Scrolls the first range into view.
  1757. if ( this.type != CKEDITOR.SELECTION_NONE )
  1758. this.getRanges()[ 0 ].scrollIntoView();
  1759. },
  1760. /**
  1761. * Remove all the selection ranges from the document.
  1762. */
  1763. removeAllRanges: function() {
  1764. // Don't clear selection outside this selection's root (#11500).
  1765. if ( this.getType() == CKEDITOR.SELECTION_NONE )
  1766. return;
  1767. var nativ = this.getNative();
  1768. try {
  1769. nativ && nativ[ isMSSelection ? 'empty' : 'removeAllRanges' ]();
  1770. } catch ( er ) {}
  1771. this.reset();
  1772. }
  1773. };
  1774. } )();
  1775. /**
  1776. * Fired when selection inside editor has been changed. Note that this event
  1777. * is fired only when selection's start element (container of a selecion start)
  1778. * changes, not on every possible selection change. Thanks to that `selectionChange`
  1779. * is fired less frequently, but on every context
  1780. * (the {@link CKEDITOR.editor#elementPath elements path} holding selection's start) change.
  1781. *
  1782. * @event selectionChange
  1783. * @member CKEDITOR.editor
  1784. * @param {CKEDITOR.editor} editor This editor instance.
  1785. * @param data
  1786. * @param {CKEDITOR.dom.selection} data.selection
  1787. * @param {CKEDITOR.dom.elementPath} data.path
  1788. */
  1789. /**
  1790. * Selection's revision. This value is incremented every time new
  1791. * selection is created or existing one is modified.
  1792. *
  1793. * @since 4.3
  1794. * @readonly
  1795. * @property {Number} rev
  1796. */
  1797. /**
  1798. * Document in which selection is anchored.
  1799. *
  1800. * @readonly
  1801. * @property {CKEDITOR.dom.document} document
  1802. */
  1803. /**
  1804. * Selection's root element.
  1805. *
  1806. * @readonly
  1807. * @property {CKEDITOR.dom.element} root
  1808. */
  1809. /**
  1810. * Whether selection is locked (cannot be modified).
  1811. *
  1812. * See {@link #lock} and {@link #unlock} methods.
  1813. *
  1814. * @readonly
  1815. * @property {Boolean} isLocked
  1816. */
  1817. /**
  1818. * Whether selection is a fake selection.
  1819. *
  1820. * See {@link #fake} method.
  1821. *
  1822. * @readonly
  1823. * @property {Boolean} isFake
  1824. */