placeholder.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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. import {
  6. enablePlaceholder,
  7. disablePlaceholder,
  8. showPlaceholder,
  9. hidePlaceholder,
  10. needsPlaceholder
  11. } from '../../src/view/placeholder';
  12. import createViewRoot from './_utils/createroot';
  13. import View from '../../src/view/view';
  14. import ViewRange from '../../src/view/range';
  15. import { setData } from '../../src/dev-utils/view';
  16. import { StylesProcessor } from '../../src/view/stylesmap';
  17. describe( 'placeholder', () => {
  18. let view, viewDocument, viewRoot;
  19. let stylesProcessor;
  20. before( () => {
  21. stylesProcessor = new StylesProcessor();
  22. } );
  23. beforeEach( () => {
  24. view = new View( stylesProcessor );
  25. viewDocument = view.document;
  26. viewRoot = createViewRoot( viewDocument );
  27. viewDocument.isFocused = true;
  28. } );
  29. describe( 'enablePlaceholder', () => {
  30. it( 'should attach proper CSS class and data attribute', () => {
  31. setData( view, '<div></div><div>{another div}</div>' );
  32. const element = viewRoot.getChild( 0 );
  33. enablePlaceholder( {
  34. view,
  35. element,
  36. text: 'foo bar baz'
  37. } );
  38. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  39. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  40. } );
  41. it( 'should attach proper CSS class and data attribute (isDirectHost=false)', () => {
  42. setData( view, '<p></p>' );
  43. viewDocument.isFocused = false;
  44. enablePlaceholder( {
  45. view,
  46. element: viewRoot,
  47. text: 'foo bar baz',
  48. isDirectHost: false
  49. } );
  50. expect( viewRoot.getChild( 0 ).getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  51. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.be.true;
  52. } );
  53. it( 'if element has children set only data attribute', () => {
  54. setData( view, '<div>first div</div><div>{another div}</div>' );
  55. const element = viewRoot.getChild( 0 );
  56. enablePlaceholder( {
  57. view,
  58. element,
  59. text: 'foo bar baz'
  60. } );
  61. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  62. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  63. } );
  64. it( 'if element has only ui elements, set CSS class and data attribute', () => {
  65. setData( view, '<div><ui:span></ui:span><ui:span></ui:span></div><div>{another div}</div>' );
  66. const element = viewRoot.getChild( 0 );
  67. enablePlaceholder( {
  68. view,
  69. element,
  70. text: 'foo bar baz'
  71. } );
  72. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  73. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  74. } );
  75. it( 'if element has selection inside set only data attribute', () => {
  76. setData( view, '<div>[]</div><div>another div</div>' );
  77. const element = viewRoot.getChild( 0 );
  78. enablePlaceholder( {
  79. view,
  80. element,
  81. text: 'foo bar baz'
  82. } );
  83. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  84. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  85. } );
  86. it( 'if element has selection inside but document is blurred should contain placeholder CSS class', () => {
  87. setData( view, '<div>[]</div><div>another div</div>' );
  88. const element = viewRoot.getChild( 0 );
  89. viewDocument.isFocused = false;
  90. enablePlaceholder( {
  91. view,
  92. element,
  93. text: 'foo bar baz'
  94. } );
  95. view.forceRender();
  96. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  97. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  98. } );
  99. it( 'should remove CSS class if selection is moved inside', () => {
  100. setData( view, '<div></div><div>{another div}</div>' );
  101. const element = viewRoot.getChild( 0 );
  102. enablePlaceholder( {
  103. view,
  104. element,
  105. text: 'foo bar baz'
  106. } );
  107. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  108. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  109. view.change( writer => {
  110. writer.setSelection( ViewRange._createIn( element ) );
  111. } );
  112. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  113. } );
  114. it( 'should change placeholder settings when called twice', () => {
  115. setData( view, '<div></div><div>{another div}</div>' );
  116. const element = viewRoot.getChild( 0 );
  117. enablePlaceholder( {
  118. view,
  119. element,
  120. text: 'foo bar baz'
  121. } );
  122. enablePlaceholder( {
  123. view,
  124. element,
  125. text: 'new text'
  126. } );
  127. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'new text' );
  128. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  129. } );
  130. it( 'should not throw when element is no longer in document', () => {
  131. setData( view, '<div></div><div>{another div}</div>' );
  132. const element = viewRoot.getChild( 0 );
  133. enablePlaceholder( {
  134. view,
  135. element,
  136. text: 'foo bar baz'
  137. } );
  138. setData( view, '<p>paragraph</p>' );
  139. view.forceRender();
  140. } );
  141. it( 'should allow to add placeholder to elements from different documents', () => {
  142. setData( view, '<div></div><div>{another div}</div>' );
  143. const element = viewRoot.getChild( 0 );
  144. const secondView = new View( stylesProcessor );
  145. const secondDocument = secondView.document;
  146. secondDocument.isFocused = true;
  147. const secondRoot = createViewRoot( secondDocument );
  148. setData( secondView, '<div></div><div>{another div}</div>' );
  149. const secondElement = secondRoot.getChild( 0 );
  150. enablePlaceholder( {
  151. view,
  152. element,
  153. text: 'first placeholder'
  154. } );
  155. enablePlaceholder( {
  156. view: secondView,
  157. element: secondElement,
  158. text: 'second placeholder'
  159. } );
  160. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' );
  161. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  162. expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' );
  163. expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true;
  164. // Move selection to the elements with placeholders.
  165. view.change( writer => {
  166. writer.setSelection( ViewRange._createIn( element ) );
  167. } );
  168. secondView.change( writer => {
  169. writer.setSelection( ViewRange._createIn( secondElement ) );
  170. } );
  171. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' );
  172. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  173. expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' );
  174. expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.false;
  175. } );
  176. it( 'should update placeholder before rendering', () => {
  177. setData( view, '<div></div><div>{another div}</div>' );
  178. const element = viewRoot.getChild( 0 );
  179. enablePlaceholder( {
  180. view,
  181. element,
  182. text: 'foo bar baz'
  183. } );
  184. view.change( writer => {
  185. writer.setSelection( ViewRange._createIn( element ) );
  186. // Here we are before rendering - placeholder is visible in first element;
  187. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  188. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  189. } );
  190. // After rendering - placeholder should be invisible since selection is moved there.
  191. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  192. } );
  193. it( 'should not set attributes/class when multiple children (isDirectHost=false)', () => {
  194. setData( view, '<p></p><p></p>' );
  195. viewDocument.isFocused = false;
  196. enablePlaceholder( {
  197. view,
  198. element: viewRoot,
  199. text: 'foo bar baz',
  200. isDirectHost: false
  201. } );
  202. expect( viewRoot.getChild( 0 ).hasAttribute( 'data-placeholder' ) ).to.be.false;
  203. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.be.false;
  204. } );
  205. it( 'should not set attributes/class when first child is not element (isDirectHost=false)', () => {
  206. setData( view, '<ui:span></ui:span>' );
  207. viewDocument.isFocused = false;
  208. enablePlaceholder( {
  209. view,
  210. element: viewRoot,
  211. text: 'foo bar baz',
  212. isDirectHost: false
  213. } );
  214. expect( viewRoot.getChild( 0 ).hasAttribute( 'data-placeholder' ) ).to.be.false;
  215. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.be.false;
  216. } );
  217. } );
  218. describe( 'disablePlaceholder', () => {
  219. it( 'should remove placeholder from element', () => {
  220. setData( view, '<div></div><div>{another div}</div>' );
  221. const element = viewRoot.getChild( 0 );
  222. enablePlaceholder( {
  223. view,
  224. element,
  225. text: 'foo bar baz'
  226. } );
  227. expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  228. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  229. disablePlaceholder( view, element );
  230. expect( element.hasAttribute( 'data-placeholder' ) ).to.be.false;
  231. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  232. } );
  233. it( 'should not blow up when called on element without placeholder', () => {
  234. setData( view, '<div></div><div>{another div}</div>' );
  235. const element = viewRoot.getChild( 0 );
  236. disablePlaceholder( view, element );
  237. expect( element.hasAttribute( 'data-placeholder' ) ).to.be.false;
  238. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  239. } );
  240. it( 'should remove placeholder from element (isDirectHost=false)', () => {
  241. setData( view, '<p></p>' );
  242. viewDocument.isFocused = false;
  243. enablePlaceholder( {
  244. view,
  245. element: viewRoot,
  246. text: 'foo bar baz',
  247. isDirectHost: false
  248. } );
  249. expect( viewRoot.getChild( 0 ).getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' );
  250. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.be.true;
  251. disablePlaceholder( view, viewRoot );
  252. expect( viewRoot.getChild( 0 ).hasAttribute( 'data-placeholder' ) ).to.be.false;
  253. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.be.false;
  254. } );
  255. } );
  256. describe( 'showPlaceholder', () => {
  257. it( 'should add the ck-placholder class if an element does not have it', () => {
  258. setData( view, '<div></div>' );
  259. const element = viewRoot.getChild( 0 );
  260. const result = view.change( writer => showPlaceholder( writer, element ) );
  261. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  262. expect( result ).to.be.true;
  263. } );
  264. it( 'should do nothing if an element already has the ck-placeholder class', () => {
  265. setData( view, '<div class="ck-placeholder"></div>' );
  266. const element = viewRoot.getChild( 0 );
  267. let spy;
  268. const result = view.change( writer => {
  269. spy = sinon.spy( writer, 'addClass' );
  270. return showPlaceholder( writer, element );
  271. } );
  272. expect( element.hasClass( 'ck-placeholder' ) ).to.be.true;
  273. expect( result ).to.be.false;
  274. sinon.assert.notCalled( spy );
  275. } );
  276. } );
  277. describe( 'hidePlaceholder', () => {
  278. it( 'should remove the ck-placholder class if an element has it', () => {
  279. setData( view, '<div class="ck-placeholder"></div>' );
  280. const element = viewRoot.getChild( 0 );
  281. const result = view.change( writer => hidePlaceholder( writer, element ) );
  282. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  283. expect( result ).to.be.true;
  284. } );
  285. it( 'should do nothing if an element has no ck-placeholder class', () => {
  286. setData( view, '<div></div>' );
  287. const element = viewRoot.getChild( 0 );
  288. let spy;
  289. const result = view.change( writer => {
  290. spy = sinon.spy( writer, 'removeClass' );
  291. return hidePlaceholder( writer, element );
  292. } );
  293. expect( element.hasClass( 'ck-placeholder' ) ).to.be.false;
  294. expect( result ).to.be.false;
  295. sinon.assert.notCalled( spy );
  296. } );
  297. } );
  298. describe( 'needsPlaceholder', () => {
  299. it( 'should return false if element was removed from the document', () => {
  300. setData( view, '<p></p>' );
  301. viewDocument.isFocused = false;
  302. const element = viewRoot.getChild( 0 );
  303. expect( needsPlaceholder( element ) ).to.be.true;
  304. view.change( writer => {
  305. writer.remove( element );
  306. } );
  307. expect( needsPlaceholder( element ) ).to.be.false;
  308. } );
  309. it( 'should return true if element is empty and document is blurred', () => {
  310. setData( view, '<p></p>' );
  311. viewDocument.isFocused = false;
  312. const element = viewRoot.getChild( 0 );
  313. expect( needsPlaceholder( element ) ).to.be.true;
  314. } );
  315. it( 'should return true if element hosts UI elements only and document is blurred', () => {
  316. setData( view, '<p><ui:span></ui:span></p>' );
  317. viewDocument.isFocused = false;
  318. const element = viewRoot.getChild( 0 );
  319. expect( needsPlaceholder( element ) ).to.be.true;
  320. } );
  321. it( 'should return true when document is focused but selection anchored somewhere else', () => {
  322. setData( view, '<p></p><p>{moo}</p>' );
  323. viewDocument.isFocused = true;
  324. const element = viewRoot.getChild( 0 );
  325. expect( needsPlaceholder( element ) ).to.be.true;
  326. } );
  327. } );
  328. } );