jumpoveruielement.js 15 KB

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