domconverter.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document */
  6. import DomConverter from '../../../src/view/domconverter';
  7. import ViewEditable from '../../../src/view/editableelement';
  8. import ViewDocument from '../../../src/view/document';
  9. import ViewUIElement from '../../../src/view/uielement';
  10. import ViewContainerElement from '../../../src/view/containerelement';
  11. import { BR_FILLER, INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '../../../src/view/filler';
  12. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  13. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  14. import { StylesProcessor } from '../../../src/view/stylesmap';
  15. describe( 'DomConverter', () => {
  16. let converter, viewDocument;
  17. testUtils.createSinonSandbox();
  18. beforeEach( () => {
  19. viewDocument = new ViewDocument( new StylesProcessor() );
  20. converter = new DomConverter( viewDocument );
  21. } );
  22. describe( 'constructor()', () => {
  23. it( 'should create converter with BR block filler mode by default', () => {
  24. expect( converter.blockFillerMode ).to.equal( 'br' );
  25. } );
  26. it( 'should create converter with defined block mode filler', () => {
  27. converter = new DomConverter( viewDocument, { blockFillerMode: 'nbsp' } );
  28. expect( converter.blockFillerMode ).to.equal( 'nbsp' );
  29. } );
  30. } );
  31. describe( 'focus()', () => {
  32. let viewEditable, domEditable, domEditableParent, viewDocument;
  33. beforeEach( () => {
  34. viewDocument = new ViewDocument( new StylesProcessor() );
  35. viewEditable = new ViewEditable( viewDocument, 'div' );
  36. domEditable = document.createElement( 'div' );
  37. domEditableParent = document.createElement( 'div' );
  38. converter.bindElements( domEditable, viewEditable );
  39. domEditable.setAttribute( 'contenteditable', 'true' );
  40. domEditableParent.appendChild( domEditable );
  41. document.body.appendChild( domEditableParent );
  42. } );
  43. afterEach( () => {
  44. converter.unbindDomElement( domEditable );
  45. document.body.removeChild( domEditableParent );
  46. document.body.focus();
  47. } );
  48. it( 'should call focus on corresponding DOM editable', () => {
  49. const focusSpy = testUtils.sinon.spy( domEditable, 'focus' );
  50. converter.focus( viewEditable );
  51. expect( focusSpy.calledOnce ).to.be.true;
  52. } );
  53. it( 'should not focus already focused editable', () => {
  54. const focusSpy = testUtils.sinon.spy( domEditable, 'focus' );
  55. converter.focus( viewEditable );
  56. converter.focus( viewEditable );
  57. expect( focusSpy.calledOnce ).to.be.true;
  58. } );
  59. // https://github.com/ckeditor/ckeditor5-engine/issues/951
  60. // https://github.com/ckeditor/ckeditor5-engine/issues/957
  61. it( 'should actively prevent scrolling', () => {
  62. const scrollToSpy = testUtils.sinon.stub( global.window, 'scrollTo' );
  63. const editableScrollLeftSpy = sinon.spy();
  64. const editableScrollTopSpy = sinon.spy();
  65. const parentScrollLeftSpy = sinon.spy();
  66. const parentScrollTopSpy = sinon.spy();
  67. const documentElementScrollLeftSpy = sinon.spy();
  68. const documentElementScrollTopSpy = sinon.spy();
  69. Object.defineProperties( domEditable, {
  70. scrollLeft: {
  71. get: () => 20,
  72. set: editableScrollLeftSpy
  73. },
  74. scrollTop: {
  75. get: () => 200,
  76. set: editableScrollTopSpy
  77. }
  78. } );
  79. Object.defineProperties( domEditableParent, {
  80. scrollLeft: {
  81. get: () => 40,
  82. set: parentScrollLeftSpy
  83. },
  84. scrollTop: {
  85. get: () => 400,
  86. set: parentScrollTopSpy
  87. }
  88. } );
  89. Object.defineProperties( global.document.documentElement, {
  90. scrollLeft: {
  91. get: () => 60,
  92. set: documentElementScrollLeftSpy
  93. },
  94. scrollTop: {
  95. get: () => 600,
  96. set: documentElementScrollTopSpy
  97. }
  98. } );
  99. testUtils.sinon.stub( global.window, 'scrollX' ).get( () => 10 );
  100. testUtils.sinon.stub( global.window, 'scrollY' ).get( () => 100 );
  101. converter.focus( viewEditable );
  102. sinon.assert.calledWithExactly( scrollToSpy, 10, 100 );
  103. sinon.assert.calledWithExactly( editableScrollLeftSpy, 20 );
  104. sinon.assert.calledWithExactly( editableScrollTopSpy, 200 );
  105. sinon.assert.calledWithExactly( parentScrollLeftSpy, 40 );
  106. sinon.assert.calledWithExactly( parentScrollTopSpy, 400 );
  107. sinon.assert.calledWithExactly( documentElementScrollLeftSpy, 60 );
  108. sinon.assert.calledWithExactly( documentElementScrollTopSpy, 600 );
  109. } );
  110. } );
  111. describe( 'DOM nodes type checking', () => {
  112. let text, element, documentFragment, comment;
  113. before( () => {
  114. text = document.createTextNode( 'test' );
  115. element = document.createElement( 'div' );
  116. documentFragment = document.createDocumentFragment();
  117. comment = document.createComment( 'a' );
  118. } );
  119. describe( 'isElement()', () => {
  120. it( 'should return true for HTMLElement nodes', () => {
  121. expect( converter.isElement( element ) ).to.be.true;
  122. } );
  123. it( 'should return false for other arguments', () => {
  124. expect( converter.isElement( text ) ).to.be.false;
  125. expect( converter.isElement( documentFragment ) ).to.be.false;
  126. expect( converter.isElement( comment ) ).to.be.false;
  127. expect( converter.isElement( {} ) ).to.be.false;
  128. } );
  129. } );
  130. describe( 'isDocumentFragment()', () => {
  131. it( 'should return true for HTMLElement nodes', () => {
  132. expect( converter.isDocumentFragment( documentFragment ) ).to.be.true;
  133. } );
  134. it( 'should return false for other arguments', () => {
  135. expect( converter.isDocumentFragment( text ) ).to.be.false;
  136. expect( converter.isDocumentFragment( element ) ).to.be.false;
  137. expect( converter.isDocumentFragment( comment ) ).to.be.false;
  138. expect( converter.isDocumentFragment( {} ) ).to.be.false;
  139. } );
  140. } );
  141. describe( 'isComment()', () => {
  142. it( 'should return true for HTML comments', () => {
  143. expect( converter.isComment( comment ) ).to.be.true;
  144. } );
  145. it( 'should return false for other arguments', () => {
  146. expect( converter.isComment( text ) ).to.be.false;
  147. expect( converter.isComment( element ) ).to.be.false;
  148. expect( converter.isComment( documentFragment ) ).to.be.false;
  149. expect( converter.isComment( {} ) ).to.be.false;
  150. } );
  151. } );
  152. } );
  153. describe( 'isDomSelectionCorrect()', () => {
  154. function domSelection( anchorParent, anchorOffset, focusParent, focusOffset ) {
  155. const sel = document.getSelection();
  156. sel.collapse( anchorParent, anchorOffset );
  157. sel.extend( focusParent, focusOffset );
  158. return sel;
  159. }
  160. let domP, domFillerTextNode, domUiSpan, domUiDeepSpan, viewDocument;
  161. beforeEach( () => {
  162. // <p>INLINE_FILLERfoo<span></span></p>.
  163. domP = document.createElement( 'p' );
  164. domFillerTextNode = document.createTextNode( INLINE_FILLER + 'foo' );
  165. domUiSpan = document.createElement( 'span' );
  166. domUiDeepSpan = document.createElement( 'span' );
  167. domUiSpan.appendChild( domUiDeepSpan );
  168. viewDocument = new ViewDocument( new StylesProcessor() );
  169. const viewUiSpan = new ViewUIElement( viewDocument, 'span' );
  170. const viewElementSpan = new ViewContainerElement( viewDocument, 'span' );
  171. domP.appendChild( domFillerTextNode );
  172. domP.appendChild( domUiSpan );
  173. converter.bindElements( domUiSpan, viewUiSpan );
  174. converter.bindElements( domUiDeepSpan, viewElementSpan );
  175. document.body.appendChild( domP );
  176. } );
  177. afterEach( () => {
  178. domP.remove();
  179. } );
  180. it( 'should return true for correct dom selection', () => {
  181. // <p>INLINE_FILLER{foo}<span></span></p>.
  182. const sel1 = domSelection( domFillerTextNode, INLINE_FILLER_LENGTH, domFillerTextNode, INLINE_FILLER_LENGTH + 3 );
  183. expect( converter.isDomSelectionCorrect( sel1 ) ).to.be.true;
  184. // <p>INLINE_FILLERfoo[]<span></span></p>.
  185. const sel2 = domSelection( domP, 1, domP, 1 );
  186. expect( converter.isDomSelectionCorrect( sel2 ) ).to.be.true;
  187. // <p>INLINE_FILLERfoo<span></span>[]</p>.
  188. const sel3 = domSelection( domP, 2, domP, 2 );
  189. expect( converter.isDomSelectionCorrect( sel3 ) ).to.be.true;
  190. } );
  191. describe( 'should return false', () => {
  192. it( 'if anchor or focus is before filler node', () => {
  193. // Tests forward and backward selection.
  194. // <p>[INLINE_FILLERfoo]<span-ui><span-container></span></span></p>.
  195. const sel1 = domSelection( domP, 0, domP, 1 );
  196. expect( converter.isDomSelectionCorrect( sel1 ) ).to.be.false;
  197. const sel2 = domSelection( domP, 1, domP, 0 );
  198. expect( converter.isDomSelectionCorrect( sel2 ) ).to.be.false;
  199. } );
  200. it( 'if anchor or focus is before filler sequence', () => {
  201. // Tests forward and backward selection.
  202. // <p>{INLINE_FILLERfoo}<span-ui><span-container></span></span></p>.
  203. const sel1 = domSelection( domFillerTextNode, 0, domFillerTextNode, INLINE_FILLER_LENGTH + 3 );
  204. expect( converter.isDomSelectionCorrect( sel1 ) ).to.be.false;
  205. const sel2 = domSelection( domFillerTextNode, INLINE_FILLER_LENGTH + 3, domFillerTextNode, 0 );
  206. expect( converter.isDomSelectionCorrect( sel2 ) ).to.be.false;
  207. } );
  208. it( 'if anchor or focus is in the middle of filler sequence', () => {
  209. // Tests forward and backward selection.
  210. // <p>I{NLINE_FILLERfoo}<span-ui><span-container></span></span></p>.
  211. const sel1 = domSelection( domFillerTextNode, 1, domFillerTextNode, INLINE_FILLER_LENGTH + 3 );
  212. expect( converter.isDomSelectionCorrect( sel1 ) ).to.be.false;
  213. const sel2 = domSelection( domFillerTextNode, INLINE_FILLER_LENGTH + 3, domFillerTextNode, 1 );
  214. expect( converter.isDomSelectionCorrect( sel2 ) ).to.be.false;
  215. } );
  216. it( 'if anchor or focus is directly inside dom element that represents view ui element', () => {
  217. // Set text indside ui element to put selection there.
  218. domUiSpan.innerText = 'xxx';
  219. // Tests forward and backward selection.
  220. // <p>INLINE_FILLER{foo<span-ui>xxx]<span-container></span></span></p>.
  221. const sel1 = domSelection( domFillerTextNode, INLINE_FILLER_LENGTH, domUiSpan, 1 );
  222. expect( converter.isDomSelectionCorrect( sel1 ) ).to.be.false;
  223. const sel2 = domSelection( domUiSpan, 1, domFillerTextNode, INLINE_FILLER_LENGTH );
  224. expect( converter.isDomSelectionCorrect( sel2 ) ).to.be.false;
  225. } );
  226. it( 'if anchor or focus is inside deep ui element structure (not directly in ui element)', () => {
  227. // Set text indside ui element to put selection there.
  228. domUiDeepSpan.innerText = 'xxx';
  229. // Tests forward and backward selection.
  230. // <p>INLINE_FILLER{foo<span-ui><span-container>xxx]</span></span></p>.
  231. const sel1 = domSelection( domFillerTextNode, INLINE_FILLER_LENGTH, domUiDeepSpan, 1 );
  232. expect( converter.isDomSelectionCorrect( sel1 ) ).to.be.false;
  233. const sel2 = domSelection( domUiDeepSpan, 1, domFillerTextNode, INLINE_FILLER_LENGTH );
  234. expect( converter.isDomSelectionCorrect( sel2 ) ).to.be.false;
  235. } );
  236. } );
  237. } );
  238. describe( 'isBlockFiller()', () => {
  239. describe( 'mode "nbsp"', () => {
  240. beforeEach( () => {
  241. converter = new DomConverter( viewDocument, { blockFillerMode: 'nbsp' } );
  242. } );
  243. it( 'should return true if the node is an nbsp filler and is a single child of a block level element', () => {
  244. const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap
  245. const context = document.createElement( 'div' );
  246. context.appendChild( nbspFillerInstance );
  247. expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.true;
  248. } );
  249. it( 'should return false if the node is an nbsp filler and is not a single child of a block level element', () => {
  250. const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap
  251. const context = document.createElement( 'div' );
  252. context.appendChild( nbspFillerInstance );
  253. context.appendChild( document.createTextNode( 'a' ) );
  254. expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false;
  255. } );
  256. it( 'should return false if there are two nbsp fillers in a block element', () => {
  257. const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap
  258. const context = document.createElement( 'div' );
  259. context.appendChild( nbspFillerInstance );
  260. context.appendChild( NBSP_FILLER( document ) ); // eslint-disable-line new-cap
  261. expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false;
  262. } );
  263. it( 'should return false filler is placed in a non-block element', () => {
  264. const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap
  265. const context = document.createElement( 'span' );
  266. context.appendChild( nbspFillerInstance );
  267. expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false;
  268. } );
  269. it( 'should return false if the node is an instance of the BR block filler', () => {
  270. const brFillerInstance = BR_FILLER( document ); // eslint-disable-line new-cap
  271. expect( converter.isBlockFiller( brFillerInstance ) ).to.be.false;
  272. } );
  273. it( 'should return false for inline filler', () => {
  274. expect( converter.isBlockFiller( document.createTextNode( INLINE_FILLER ) ) ).to.be.false;
  275. } );
  276. it( 'should return false for a normal <br> element', () => {
  277. const context = document.createElement( 'div' );
  278. context.innerHTML = 'x<br>x';
  279. expect( converter.isBlockFiller( context.childNodes[ 1 ] ) ).to.be.false;
  280. } );
  281. // SPECIAL CASE (see ckeditor5#5564).
  282. it( 'should return true for a <br> element which is the only child of its block parent', () => {
  283. const context = document.createElement( 'div' );
  284. context.innerHTML = '<br>';
  285. expect( converter.isBlockFiller( context.firstChild ) ).to.be.true;
  286. } );
  287. it( 'should return false for a <br> element which is the only child of its non-block parent', () => {
  288. const context = document.createElement( 'span' );
  289. context.innerHTML = '<br>';
  290. expect( converter.isBlockFiller( context.firstChild ) ).to.be.false;
  291. } );
  292. it( 'should return false for a <br> element which is followed by an nbsp', () => {
  293. const context = document.createElement( 'span' );
  294. context.innerHTML = '<br>&nbsp;';
  295. expect( converter.isBlockFiller( context.firstChild ) ).to.be.false;
  296. } );
  297. } );
  298. describe( 'mode "br"', () => {
  299. beforeEach( () => {
  300. converter = new DomConverter( viewDocument, { blockFillerMode: 'br' } );
  301. } );
  302. it( 'should return true if the node is an instance of the BR block filler', () => {
  303. const brFillerInstance = BR_FILLER( document ); // eslint-disable-line new-cap
  304. expect( converter.isBlockFiller( brFillerInstance ) ).to.be.true;
  305. // Check it twice to ensure that caching breaks nothing.
  306. expect( converter.isBlockFiller( brFillerInstance ) ).to.be.true;
  307. } );
  308. it( 'should return false if the node is an instance of the NBSP block filler', () => {
  309. converter = new DomConverter( viewDocument, { blockFillerMode: 'br' } );
  310. const nbspFillerInstance = NBSP_FILLER( document ); // eslint-disable-line new-cap
  311. // NBSP must be check inside a context.
  312. const context = document.createElement( 'div' );
  313. context.appendChild( nbspFillerInstance );
  314. expect( converter.isBlockFiller( nbspFillerInstance ) ).to.be.false;
  315. } );
  316. it( 'should return false for inline filler', () => {
  317. expect( converter.isBlockFiller( document.createTextNode( INLINE_FILLER ) ) ).to.be.false;
  318. } );
  319. } );
  320. } );
  321. } );