jumpoveruielement.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals document */
  6. import ViewDocument from '../../../src/view/document';
  7. import UIElement from '../../../src/view/uielement';
  8. import ViewContainerElement from '../../../src/view/containerelement';
  9. import ViewAttribtueElement from '../../../src/view/attributeelement';
  10. import ViewText from '../../../src/view/text';
  11. import ViewRange from '../../../src/view/range';
  12. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  13. import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
  14. import { setData as setViewData } from '../../../src/dev-utils/view';
  15. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  16. describe( 'Document', () => {
  17. let viewDocument, domRoot, domSelection, viewRoot, foo, bar, ui, ui2;
  18. class MyUIElement extends UIElement {
  19. render( domDocument ) {
  20. const element = super.render( domDocument );
  21. element.innerText = this.contents;
  22. return element;
  23. }
  24. }
  25. beforeEach( () => {
  26. domRoot = createElement( document, 'div', {
  27. contenteditable: 'true'
  28. } );
  29. document.body.appendChild( domRoot );
  30. viewDocument = new ViewDocument();
  31. viewRoot = viewDocument.createRoot( domRoot );
  32. domSelection = document.getSelection();
  33. domSelection.removeAllRanges();
  34. viewDocument.isFocused = true;
  35. foo = new ViewText( 'foo' );
  36. bar = new ViewText( 'bar' );
  37. ui = new MyUIElement( 'span' );
  38. ui.contents = 'xxx';
  39. ui2 = new MyUIElement( 'span' );
  40. ui2.contents = 'yyy';
  41. } );
  42. afterEach( () => {
  43. viewDocument.destroy();
  44. domRoot.parentElement.removeChild( domRoot );
  45. } );
  46. function renderAndFireKeydownEvent( options ) {
  47. viewDocument.render();
  48. const eventData = Object.assign( { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) }, options );
  49. viewDocument.fire( 'keydown', eventData );
  50. }
  51. function check( anchorNode, anchorOffset, focusNode, focusOffset ) {
  52. const anchor = domSelection.anchorNode.data ? domSelection.anchorNode.data : domSelection.anchorNode.nodeName.toUpperCase();
  53. expect( anchor, 'anchorNode' ).to.equal( anchorNode );
  54. expect( domSelection.anchorOffset, 'anchorOffset' ).to.equal( anchorOffset );
  55. if ( focusNode ) {
  56. const focus = domSelection.focusNode.data ? domSelection.focusNode.data : domSelection.focusNode.nodeName.toUpperCase();
  57. expect( focus, 'focusNode' ).to.equal( focusNode );
  58. expect( domSelection.focusOffset, 'focusOffset' ).to.equal( focusOffset );
  59. } else {
  60. expect( domSelection.isCollapsed, 'isCollapsed' ).to.be.true;
  61. }
  62. }
  63. describe( 'jump over ui element handler', () => {
  64. describe( 'collapsed selection', () => {
  65. it( 'do nothing when another key is pressed', () => {
  66. // <container:p>foo<ui:span>xxx</ui:span>{}bar</container:p>
  67. const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
  68. viewRoot.appendChildren( p );
  69. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] );
  70. renderAndFireKeydownEvent( { keyCode: keyCodes.arrowleft } );
  71. testUtils.checkAssertions(
  72. () => check( 'bar', 0 ),
  73. // Safari renders selection at the end of the text node.
  74. () => check( 'xxx', 3 )
  75. );
  76. } );
  77. it( 'jump over ui element when right arrow is pressed before ui element - directly before ui element', () => {
  78. // <container:p>foo[]<ui:span>xxx</ui:span>bar</container:p>
  79. const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
  80. viewRoot.appendChildren( p );
  81. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] );
  82. renderAndFireKeydownEvent();
  83. testUtils.checkAssertions(
  84. // <p>foo<span>xxx</span>[]bar</p>
  85. () => check( 'P', 2 ),
  86. // Safari renders selection at the end of the text node.
  87. // <p>foo<span>xxx{}</span>bar</p>
  88. () => check( 'xxx', 3 )
  89. );
  90. } );
  91. it( 'jump over ui element when right arrow is pressed before ui element - not directly before ui element', () => {
  92. // <container:p>foo{}<ui:span>xxx</ui:span>bar</container:p>
  93. const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
  94. viewRoot.appendChildren( p );
  95. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  96. renderAndFireKeydownEvent();
  97. testUtils.checkAssertions(
  98. // <p>foo<span>xxx</span>[]bar</p>
  99. () => check( 'P', 2 ),
  100. // Safari renders selection at the end of the text node.
  101. // <p>foo<span>xxx{}</span>bar</p>
  102. () => check( 'xxx', 3 )
  103. );
  104. } );
  105. it( 'jump over multiple ui elements when right arrow is pressed before ui element', () => {
  106. // <container:p>foo{}<ui:span>xxx</ui:span><ui:span>yyy</ui:span>bar</container:p>'
  107. const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2, bar ] );
  108. viewRoot.appendChildren( p );
  109. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  110. renderAndFireKeydownEvent();
  111. testUtils.checkAssertions(
  112. // <p>foo<span>xxx</span><span>yyy</span>[]bar</p>
  113. () => check( 'P', 3 ),
  114. // Safari renders selection at the end of the text node.
  115. // <p>foo<span>xxx</span><span>yyy{}</span>bar</p>
  116. () => check( 'yyy', 3 )
  117. );
  118. } );
  119. it( 'jump over ui elements at the end of container element', () => {
  120. // <container:p>foo{}<ui:span>xxx</ui:span><ui:span>yyy</ui:span></container:p><container:div></container:div>
  121. const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2 ] );
  122. const div = new ViewContainerElement( 'div' );
  123. viewRoot.appendChildren( p );
  124. viewRoot.appendChildren( div );
  125. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  126. renderAndFireKeydownEvent();
  127. testUtils.checkAssertions(
  128. // <p>foo<span>xxx</span><span>yyy</span>[]</p><div></div>
  129. () => check( 'P', 3 ),
  130. // Safari renders selection at the end of the text node.
  131. // <p>foo<span>xxx</span><span>yyy{}</span></p><div></div>
  132. () => check( 'yyy', 3 )
  133. );
  134. } );
  135. it( 'jump over ui element if selection is in attribute element - case 1', () => {
  136. // <container:p><attribute:b>foo{}</attribute:b><ui:span>xxx</ui:span>bar</container:p>
  137. const b = new ViewAttribtueElement( 'b', null, foo );
  138. const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
  139. viewRoot.appendChildren( p );
  140. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  141. renderAndFireKeydownEvent();
  142. testUtils.checkAssertions(
  143. // <p><b>foo</b><span>xxx</span>[]bar</p>
  144. () => check( 'P', 2 ),
  145. // Safari renders selection at the end of the text node.
  146. // <p><b>foo</b><span>xxx{}</span>bar</p>
  147. () => check( 'xxx', 3 )
  148. );
  149. } );
  150. it( 'jump over ui element if selection is in attribute element - case 2', () => {
  151. // <container:p><attribute:b>foo[]</attribute:b><ui:span>xxx</ui:span>bar</container:p>
  152. const b = new ViewAttribtueElement( 'b', null, foo );
  153. const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] );
  154. viewRoot.appendChildren( p );
  155. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ] );
  156. renderAndFireKeydownEvent();
  157. testUtils.checkAssertions(
  158. // <p><b>foo</b><span>xxx</span>[]bar</p>
  159. () => check( 'P', 2 ),
  160. // Safari renders selection at the end of the text node.
  161. // <p><b>foo</b><span>xxx{}</span>bar</p>
  162. () => check( 'xxx', 3 )
  163. );
  164. } );
  165. it( 'jump over ui element if selection is in multiple attribute elements', () => {
  166. // <container:p>
  167. // <attribute:i>
  168. // <attribute:b>foo{}</attribute:b>
  169. // </attribute:i>
  170. // <ui:span>
  171. // xxx
  172. // </ui:span>
  173. // bar
  174. // </container:p>
  175. const b = new ViewAttribtueElement( 'b', null, foo );
  176. const i = new ViewAttribtueElement( 'i', null, b );
  177. const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
  178. viewRoot.appendChildren( p );
  179. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  180. renderAndFireKeydownEvent();
  181. testUtils.checkAssertions(
  182. // <p><i><b>foo</b></i><span>xxx</span>[]bar</p>
  183. () => check( 'P', 2 ),
  184. // Safari renders selection at the end of the text node.
  185. // <p><i><b>foo</b></i><span>xxx{}</span>bar</p>
  186. () => check( 'xxx', 3 )
  187. );
  188. } );
  189. it( 'jump over empty attribute elements and ui elements', () => {
  190. // <container:p>' +
  191. // foo{}
  192. // <attribute:b></attribute:b>
  193. // <ui:span>xxx</ui:span>
  194. // <ui:span>yyy</ui:span>
  195. // <attribute:b></attribute:b>
  196. // bar
  197. // </container:p>
  198. const b1 = new ViewAttribtueElement( 'b' );
  199. const b2 = new ViewAttribtueElement( 'b' );
  200. const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
  201. viewRoot.appendChildren( p );
  202. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  203. renderAndFireKeydownEvent();
  204. testUtils.checkAssertions(
  205. // <p>foo<b></b><span>xxx</span><span>yyy</span>[]bar</p>
  206. () => check( 'P', 5 ),
  207. // Safari renders selection at the end of the text node.
  208. // <p>foo<b></b><span>xxx</span><span>yyy{}</span>bar</p>
  209. () => check( 'yyy', 3 )
  210. );
  211. } );
  212. it( 'jump over empty attribute elements and ui elements if shift key is pressed', () => {
  213. // <container:p>
  214. // foo{}
  215. // <attribute:b></attribute:b>
  216. // <ui:span>xxx</ui:span>
  217. // <ui:span>yyy</ui:span>
  218. // <attribute:b></attribute:b>
  219. // bar
  220. // </container:p>
  221. const b1 = new ViewAttribtueElement( 'b' );
  222. const b2 = new ViewAttribtueElement( 'b' );
  223. const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
  224. viewRoot.appendChildren( p );
  225. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] );
  226. renderAndFireKeydownEvent( { shiftKey: true } );
  227. testUtils.checkAssertions(
  228. // <p>foo<b></b><span>xxx</span><span>yyy</span><b><b>[]bar</p>
  229. () => check( 'P', 5 ),
  230. // Safari renders selection at the end of the text node.
  231. // <p>foo<b></b><span>xxx</span><span>yyy{}</span><b><b>bar</p>
  232. () => check( 'yyy', 3 )
  233. );
  234. } );
  235. it( 'do nothing if selection is not directly before ui element', () => {
  236. setViewData( viewDocument, '<container:p>fo{}o<ui:span></ui:span>bar</container:p>' );
  237. renderAndFireKeydownEvent();
  238. check( 'foo', 2 );
  239. } );
  240. it( 'do nothing if selection is in attribute element but not before ui element', () => {
  241. setViewData( viewDocument, '<container:p><attribute:b>foo{}</attribute:b>bar</container:p>' );
  242. renderAndFireKeydownEvent();
  243. check( 'foo', 3 );
  244. } );
  245. it( 'do nothing if selection is before non-empty attribute element', () => {
  246. setViewData( viewDocument, '<container:p>fo{}<attribute:b>o</attribute:b><ui:span></ui:span>bar</container:p>' );
  247. renderAndFireKeydownEvent();
  248. check( 'fo', 2 );
  249. } );
  250. it( 'do nothing if selection is before container element - case 1', () => {
  251. setViewData( viewDocument, '<container:p>foo{}</container:p><ui:span></ui:span><container:div>bar</container:div>' );
  252. renderAndFireKeydownEvent();
  253. check( 'foo', 3 );
  254. } );
  255. it( 'do nothing if selection is before container element - case 2', () => {
  256. setViewData( viewDocument, '<container:div>foo{}<container:p></container:p><ui:span></ui:span></container:div>' );
  257. renderAndFireKeydownEvent();
  258. check( 'foo', 3 );
  259. } );
  260. it( 'do nothing if selection is at the end of last container element', () => {
  261. setViewData( viewDocument, '<container:p>foo{}</container:p>' );
  262. renderAndFireKeydownEvent();
  263. check( 'foo', 3 );
  264. } );
  265. } );
  266. describe( 'non-collapsed selection', () => {
  267. it( 'should do nothing', () => {
  268. setViewData( viewDocument, '<container:p>f{oo}<ui:span></ui:span>bar</container:p>' );
  269. renderAndFireKeydownEvent();
  270. check( 'foo', 1, 'foo', 3 );
  271. } );
  272. it( 'should do nothing if selection is not before ui element - shift key pressed', () => {
  273. setViewData( viewDocument, '<container:p>f{o}o<ui:span></ui:span>bar</container:p>' );
  274. renderAndFireKeydownEvent( { shiftKey: true } );
  275. check( 'foo', 1, 'foo', 2 );
  276. } );
  277. it( 'jump over ui element if shift key is pressed', () => {
  278. // <container:p>fo{o}<ui:span>xxx</ui:span>bar</container:p>
  279. const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] );
  280. viewRoot.appendChildren( p );
  281. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
  282. renderAndFireKeydownEvent( { shiftKey: true } );
  283. testUtils.checkAssertions(
  284. // <p>fo{o<span>xxx</span>]bar</p>
  285. () => check( 'foo', 2, 'P', 2 ),
  286. // Safari renders selection at the end of the previous text node.
  287. // <p>fo{o<span>xxx}</span>bar</p>
  288. () => check( 'foo', 2, 'xxx', 3 )
  289. );
  290. } );
  291. it( 'jump over ui element if selection is in multiple attribute elements', () => {
  292. // <container:p>
  293. // <attribute:i>
  294. // <attribute:b>fo{o}</attribute:b>
  295. // </attribute:i>
  296. // <ui:span>xxx</ui:span>
  297. // bar
  298. // </container:p>
  299. const b = new ViewAttribtueElement( 'b', null, foo );
  300. const i = new ViewAttribtueElement( 'i', null, b );
  301. const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] );
  302. viewRoot.appendChildren( p );
  303. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
  304. renderAndFireKeydownEvent( { shiftKey: true } );
  305. testUtils.checkAssertions(
  306. // <p><i><b>fo{o</b></i><span>xxx</span>]bar</p>
  307. () => check( 'foo', 2, 'P', 2 ),
  308. // Safari renders selection at the end of the previous text node.
  309. // <p><i><b>fo{o</b></i><span>xxx}</span>bar</p>
  310. () => check( 'foo', 2, 'xxx', 3 )
  311. );
  312. } );
  313. it( 'jump over empty attribute elements and ui elements if shift key is pressed', () => {
  314. // <container:p>
  315. // fo{o}
  316. // <attribute:b></attribute:b>
  317. // <ui:span>xxx</ui:span>
  318. // <ui:span>yyy</ui:span>
  319. // <attribute:b></attribute:b>
  320. // bar
  321. // </container:p>
  322. const b1 = new ViewAttribtueElement( 'b' );
  323. const b2 = new ViewAttribtueElement( 'b' );
  324. const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] );
  325. viewRoot.appendChildren( p );
  326. viewDocument.selection.setRanges( [ ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ] );
  327. renderAndFireKeydownEvent( { shiftKey: true } );
  328. testUtils.checkAssertions(
  329. // <p>fo{o<b></b><span>xxx</span><span>yyy</span><b></b>]bar</p>
  330. () => check( 'foo', 2, 'P', 5 ),
  331. // Safari renders selection at the end of the previous text node.
  332. // <p>fo{o<b></b><span>xxx</span><span>yyy}</span><b></b>bar</p>
  333. () => check( 'foo', 2, 'yyy', 3 )
  334. );
  335. } );
  336. } );
  337. it( 'should do nothing if dom position cannot be converted to view position', () => {
  338. const newDiv = document.createElement( 'div' );
  339. const domSelection = document.getSelection();
  340. document.body.appendChild( newDiv );
  341. domSelection.collapse( newDiv, 0 );
  342. viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) } );
  343. } );
  344. } );
  345. } );