jumpoveruielement.js 15 KB

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