placeholder.js 12 KB

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