clipboard.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. import Delta from 'quill-delta';
  2. import { Range } from '../../../core/selection';
  3. import Quill from '../../../core';
  4. describe('Clipboard', function() {
  5. describe('events', function() {
  6. beforeEach(function() {
  7. this.quill = this.initialize(Quill, '<h1>0123</h1><p>5<em>67</em>8</p>');
  8. this.quill.setSelection(2, 5);
  9. });
  10. describe('paste', function() {
  11. beforeAll(function() {
  12. this.clipboardEvent = {
  13. clipboardData: {
  14. getData: type =>
  15. type === 'text/html' ? '<strong>|</strong>' : '|',
  16. },
  17. preventDefault: () => {},
  18. };
  19. });
  20. it('pastes html data', function(done) {
  21. this.quill.clipboard.onCapturePaste(this.clipboardEvent);
  22. setTimeout(() => {
  23. expect(this.quill.root).toEqualHTML(
  24. '<p>01<strong>|</strong><em>7</em>8</p>',
  25. );
  26. expect(this.quill.getSelection()).toEqual(new Range(3));
  27. done();
  28. }, 2);
  29. });
  30. it('pastes html data if present with file', function(done) {
  31. const upload = spyOn(this.quill.uploader, 'upload');
  32. this.quill.clipboard.onCapturePaste(
  33. // eslint-disable-next-line prefer-object-spread
  34. Object.assign({}, this.clipboardEvent, { files: ['file '] }),
  35. );
  36. setTimeout(() => {
  37. expect(upload).not.toHaveBeenCalled();
  38. expect(this.quill.root).toEqualHTML(
  39. '<p>01<strong>|</strong><em>7</em>8</p>',
  40. );
  41. expect(this.quill.getSelection()).toEqual(new Range(3));
  42. done();
  43. }, 2);
  44. });
  45. it('does not fire selection-change', function(done) {
  46. const change = jasmine.createSpy('change');
  47. this.quill.on('selection-change', change);
  48. this.quill.clipboard.onCapturePaste(this.clipboardEvent);
  49. setTimeout(function() {
  50. expect(change).not.toHaveBeenCalled();
  51. done();
  52. }, 2);
  53. });
  54. });
  55. it('dangerouslyPasteHTML(html)', function() {
  56. this.quill.clipboard.dangerouslyPasteHTML('<i>ab</i><b>cd</b>');
  57. expect(this.quill.root).toEqualHTML(
  58. '<p><em>ab</em><strong>cd</strong></p>',
  59. );
  60. });
  61. it('dangerouslyPasteHTML(index, html)', function() {
  62. this.quill.clipboard.dangerouslyPasteHTML(2, '<b>ab</b>');
  63. expect(this.quill.root).toEqualHTML(
  64. '<h1>01<strong>ab</strong>23</h1><p>5<em>67</em>8</p>',
  65. );
  66. });
  67. });
  68. describe('convert', function() {
  69. beforeEach(function() {
  70. const quill = this.initialize(Quill, '');
  71. this.clipboard = quill.clipboard;
  72. });
  73. it('plain text', function() {
  74. const delta = this.clipboard.convert({ html: 'simple plain text' });
  75. expect(delta).toEqual(new Delta().insert('simple plain text'));
  76. });
  77. it('whitespace', function() {
  78. const html =
  79. '<div> 0 </div><div> <div> 1 2 <span> 3 </span> 4 </div> </div>' +
  80. '<div><span>5 </span><span>6 </span><span> 7</span><span> 8</span></div>';
  81. const delta = this.clipboard.convert({ html });
  82. expect(delta).toEqual(new Delta().insert('0\n1 2 3 4\n5 6 7 8'));
  83. });
  84. it('inline whitespace', function() {
  85. const html = '<p>0 <strong>1</strong> 2</p>';
  86. const delta = this.clipboard.convert({ html });
  87. expect(delta).toEqual(
  88. new Delta()
  89. .insert('0 ')
  90. .insert('1', { bold: true })
  91. .insert(' 2'),
  92. );
  93. });
  94. it('intentional whitespace', function() {
  95. const html = '<span>0&nbsp;<strong>1</strong>&nbsp;2</span>';
  96. const delta = this.clipboard.convert({ html });
  97. expect(delta).toEqual(
  98. new Delta()
  99. .insert('0\u00a0')
  100. .insert('1', { bold: true })
  101. .insert('\u00a02'),
  102. );
  103. });
  104. it('consecutive intentional whitespace', function() {
  105. const html = '<strong>&nbsp;&nbsp;1&nbsp;&nbsp;</strong>';
  106. const delta = this.clipboard.convert({ html });
  107. expect(delta).toEqual(
  108. new Delta().insert('\u00a0\u00a01\u00a0\u00a0', { bold: true }),
  109. );
  110. });
  111. it('break', function() {
  112. const html =
  113. '<div>0<br>1</div><div>2<br></div><div>3</div><div><br>4</div><div><br></div><div>5</div>';
  114. const delta = this.clipboard.convert({ html });
  115. expect(delta).toEqual(new Delta().insert('0\n1\n2\n3\n\n4\n\n5'));
  116. });
  117. it('empty block', function() {
  118. const html = '<h1>Test</h1><h2></h2><p>Body</p>';
  119. const delta = this.clipboard.convert({ html });
  120. expect(delta).toEqual(
  121. new Delta()
  122. .insert('Test\n', { header: 1 })
  123. .insert('\n', { header: 2 })
  124. .insert('Body'),
  125. );
  126. });
  127. it('mixed inline and block', function() {
  128. const delta = this.clipboard.convert({
  129. html: '<div>One<div>Two</div></div>',
  130. });
  131. expect(delta).toEqual(new Delta().insert('One\nTwo'));
  132. });
  133. it('alias', function() {
  134. const delta = this.clipboard.convert({
  135. html: '<b>Bold</b><i>Italic</i>',
  136. });
  137. expect(delta).toEqual(
  138. new Delta()
  139. .insert('Bold', { bold: true })
  140. .insert('Italic', { italic: true }),
  141. );
  142. });
  143. it('pre', function() {
  144. const html = '<pre> 01 \n 23 </pre>';
  145. const delta = this.clipboard.convert({ html });
  146. expect(delta).toEqual(
  147. new Delta().insert(' 01 \n 23 \n', { 'code-block': true }),
  148. );
  149. });
  150. it('nested list', function() {
  151. const delta = this.clipboard.convert({
  152. html: '<ol><li>One</li><li class="ql-indent-1">Alpha</li></ol>',
  153. });
  154. expect(delta).toEqual(
  155. new Delta()
  156. .insert('One\n', { list: 'ordered' })
  157. .insert('Alpha\n', { list: 'ordered', indent: 1 }),
  158. );
  159. });
  160. it('html nested list', function() {
  161. const delta = this.clipboard.convert({
  162. html:
  163. '<ol><li>One<ol><li>Alpha</li><li>Beta<ol><li>I</li></ol></li></ol></li></ol>',
  164. });
  165. expect(delta).toEqual(
  166. new Delta()
  167. .insert('One\n', { list: 'ordered' })
  168. .insert('Alpha\nBeta\n', { list: 'ordered', indent: 1 })
  169. .insert('I\n', { list: 'ordered', indent: 2 }),
  170. );
  171. });
  172. it('html nested bullet', function() {
  173. const delta = this.clipboard.convert({
  174. html:
  175. '<ul><li>One<ul><li>Alpha</li><li>Beta<ul><li>I</li></ul></li></ul></li></ul>',
  176. });
  177. expect(delta).toEqual(
  178. new Delta()
  179. .insert('One\n', { list: 'bullet' })
  180. .insert('Alpha\nBeta\n', { list: 'bullet', indent: 1 })
  181. .insert('I\n', { list: 'bullet', indent: 2 }),
  182. );
  183. });
  184. it('html nested checklist', function() {
  185. const delta = this.clipboard.convert({
  186. html:
  187. '<ul><li data-list="checked">One<ul><li data-list="checked">Alpha</li><li data-list="checked">Beta' +
  188. '<ul><li data-list="checked">I</li></ul></li></ul></li></ul>',
  189. });
  190. expect(delta).toEqual(
  191. new Delta()
  192. .insert('One\n', { list: 'checked' })
  193. .insert('Alpha\nBeta\n', { list: 'checked', indent: 1 })
  194. .insert('I\n', { list: 'checked', indent: 2 }),
  195. );
  196. });
  197. it('html partial list', function() {
  198. const delta = this.clipboard.convert({
  199. html:
  200. '<ol><li><ol><li><ol><li>iiii</li></ol></li><li>bbbb</li></ol></li><li>2222</li></ol>',
  201. });
  202. expect(delta).toEqual(
  203. new Delta()
  204. .insert('iiii\n', { list: 'ordered', indent: 2 })
  205. .insert('bbbb\n', { list: 'ordered', indent: 1 })
  206. .insert('2222\n', { list: 'ordered' }),
  207. );
  208. });
  209. it('html table', function() {
  210. const delta = this.clipboard.convert({
  211. html:
  212. '<table>' +
  213. '<thead><tr><td>A1</td><td>A2</td><td>A3</td></tr></thead>' +
  214. '<tbody><tr><td>B1</td><td></td><td>B3</td></tr></tbody>' +
  215. '</table>',
  216. });
  217. expect(delta).toEqual(
  218. new Delta()
  219. .insert('A1\nA2\nA3\n', { table: 1 })
  220. .insert('B1\n\nB3\n', { table: 2 }),
  221. );
  222. });
  223. it('embeds', function() {
  224. const delta = this.clipboard.convert({
  225. html:
  226. '<div>01<img src="/assets/favicon.png" height="200" width="300">34</div>',
  227. });
  228. const expected = new Delta()
  229. .insert('01')
  230. .insert(
  231. { image: '/assets/favicon.png' },
  232. { height: '200', width: '300' },
  233. )
  234. .insert('34');
  235. expect(delta).toEqual(expected);
  236. });
  237. it('block embed', function() {
  238. const delta = this.clipboard.convert({
  239. html: '<p>01</p><iframe src="#"></iframe><p>34</p>',
  240. });
  241. expect(delta).toEqual(
  242. new Delta()
  243. .insert('01\n')
  244. .insert({ video: '#' })
  245. .insert('34'),
  246. );
  247. });
  248. it('block embeds within blocks', function() {
  249. const delta = this.clipboard.convert({
  250. html: '<h1>01<iframe src="#"></iframe>34</h1><p>67</p>',
  251. });
  252. expect(delta).toEqual(
  253. new Delta()
  254. .insert('01\n', { header: 1 })
  255. .insert({ video: '#' }, { header: 1 })
  256. .insert('34\n', { header: 1 })
  257. .insert('67'),
  258. );
  259. });
  260. it('wrapped block embed', function() {
  261. const delta = this.clipboard.convert({
  262. html: '<h1>01<a href="/"><iframe src="#"></iframe></a>34</h1><p>67</p>',
  263. });
  264. expect(delta).toEqual(
  265. new Delta()
  266. .insert('01\n', { header: 1 })
  267. .insert({ video: '#' }, { link: '/', header: 1 })
  268. .insert('34\n', { header: 1 })
  269. .insert('67'),
  270. );
  271. });
  272. it('wrapped block embed with siblings', function() {
  273. const delta = this.clipboard.convert({
  274. html:
  275. '<h1>01<a href="/">a<iframe src="#"></iframe>b</a>34</h1><p>67</p>',
  276. });
  277. expect(delta).toEqual(
  278. new Delta()
  279. .insert('01', { header: 1 })
  280. .insert('a\n', { link: '/', header: 1 })
  281. .insert({ video: '#' }, { link: '/', header: 1 })
  282. .insert('b', { link: '/', header: 1 })
  283. .insert('34\n', { header: 1 })
  284. .insert('67'),
  285. );
  286. });
  287. it('attributor and style match', function() {
  288. const delta = this.clipboard.convert({
  289. html: '<p style="direction:rtl;">Test</p>',
  290. });
  291. expect(delta).toEqual(new Delta().insert('Test\n', { direction: 'rtl' }));
  292. });
  293. it('nested styles', function() {
  294. const delta = this.clipboard.convert({
  295. html:
  296. '<span style="color: red;"><span style="color: blue;">Test</span></span>',
  297. });
  298. expect(delta).toEqual(new Delta().insert('Test', { color: 'blue' }));
  299. });
  300. it('custom matcher', function() {
  301. this.clipboard.addMatcher(Node.TEXT_NODE, function(node, delta) {
  302. let index = 0;
  303. const regex = /https?:\/\/[^\s]+/g;
  304. let match = null;
  305. const composer = new Delta();
  306. // eslint-disable-next-line no-cond-assign
  307. while ((match = regex.exec(node.data))) {
  308. composer.retain(match.index - index);
  309. index = regex.lastIndex;
  310. composer.retain(match[0].length, { link: match[0] });
  311. }
  312. return delta.compose(composer);
  313. });
  314. const delta = this.clipboard.convert({
  315. html: 'http://github.com https://quilljs.com',
  316. });
  317. const expected = new Delta()
  318. .insert('http://github.com', { link: 'http://github.com' })
  319. .insert(' ')
  320. .insert('https://quilljs.com', { link: 'https://quilljs.com' });
  321. expect(delta).toEqual(expected);
  322. });
  323. it('does not execute javascript', function() {
  324. window.unsafeFunction = jasmine.createSpy('unsafeFunction');
  325. const html =
  326. "<img src='/assets/favicon.png' onload='window.unsafeFunction()'/>";
  327. this.clipboard.convert({ html });
  328. expect(window.unsafeFunction).not.toHaveBeenCalled();
  329. delete window.unsafeFunction;
  330. });
  331. it('xss', function() {
  332. const delta = this.clipboard.convert({
  333. html: '<script>alert(2);</script>',
  334. });
  335. expect(delta).toEqual(new Delta().insert(''));
  336. });
  337. });
  338. });