8
0

linkediting.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  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 LinkEditing from '../src/linkediting';
  6. import LinkCommand from '../src/linkcommand';
  7. import UnlinkCommand from '../src/unlinkcommand';
  8. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  9. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  10. import Enter from '@ckeditor/ckeditor5-enter/src/enter';
  11. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  12. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  13. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  14. import { isLinkElement } from '../src/utils';
  15. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  16. /* global document */
  17. describe( 'LinkEditing', () => {
  18. let editor, model, view;
  19. beforeEach( () => {
  20. return VirtualTestEditor
  21. .create( {
  22. plugins: [ Paragraph, LinkEditing, Enter ]
  23. } )
  24. .then( newEditor => {
  25. editor = newEditor;
  26. model = editor.model;
  27. view = editor.editing.view;
  28. } );
  29. } );
  30. afterEach( () => {
  31. editor.destroy();
  32. } );
  33. it( 'should have pluginName', () => {
  34. expect( LinkEditing.pluginName ).to.equal( 'LinkEditing' );
  35. } );
  36. it( 'should be loaded', () => {
  37. expect( editor.plugins.get( LinkEditing ) ).to.be.instanceOf( LinkEditing );
  38. } );
  39. it( 'should set proper schema rules', () => {
  40. expect( model.schema.checkAttribute( [ '$block', '$text' ], 'linkHref' ) ).to.be.true;
  41. expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'linkHref' ) ).to.be.true;
  42. expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
  43. } );
  44. // Let's check only the minimum to not duplicate `bindTwoStepCaretToAttribute()` tests.
  45. // Testing minimum is better than testing using spies that might give false positive results.
  46. describe( 'two-step caret movement', () => {
  47. it( 'should be bound to th `linkHref` attribute (LTR)', () => {
  48. // Put selection before the link element.
  49. setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
  50. // The selection's gravity is not overridden because selection landed here not because of `keydown`.
  51. expect( editor.model.document.selection.isGravityOverridden ).to.false;
  52. // So let's simulate the `keydown` event.
  53. editor.editing.view.document.fire( 'keydown', {
  54. keyCode: keyCodes.arrowright,
  55. preventDefault: () => {},
  56. domTarget: document.body
  57. } );
  58. expect( editor.model.document.selection.isGravityOverridden ).to.true;
  59. } );
  60. it( 'should be bound to th `linkHref` attribute (RTL)', () => {
  61. return VirtualTestEditor
  62. .create( {
  63. plugins: [ Paragraph, LinkEditing, Enter ],
  64. language: {
  65. content: 'ar'
  66. }
  67. } )
  68. .then( editor => {
  69. model = editor.model;
  70. view = editor.editing.view;
  71. // Put selection before the link element.
  72. setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
  73. // The selection's gravity is not overridden because selection landed here not because of `keydown`.
  74. expect( editor.model.document.selection.isGravityOverridden ).to.false;
  75. // So let's simulate the `keydown` event.
  76. editor.editing.view.document.fire( 'keydown', {
  77. keyCode: keyCodes.arrowleft,
  78. preventDefault: () => {},
  79. domTarget: document.body
  80. } );
  81. expect( editor.model.document.selection.isGravityOverridden ).to.true;
  82. return editor.destroy();
  83. } );
  84. } );
  85. } );
  86. describe( 'command', () => {
  87. it( 'should register link command', () => {
  88. const command = editor.commands.get( 'link' );
  89. expect( command ).to.be.instanceOf( LinkCommand );
  90. } );
  91. it( 'should register unlink command', () => {
  92. const command = editor.commands.get( 'unlink' );
  93. expect( command ).to.be.instanceOf( UnlinkCommand );
  94. } );
  95. } );
  96. describe( 'data pipeline conversions', () => {
  97. it( 'should convert `<a href="url">` to `linkHref="url"` attribute', () => {
  98. editor.setData( '<p><a href="url">foo</a>bar</p>' );
  99. expect( getModelData( model, { withoutSelection: true } ) )
  100. .to.equal( '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  101. expect( editor.getData() ).to.equal( '<p><a href="url">foo</a>bar</p>' );
  102. } );
  103. it( 'should be integrated with autoparagraphing', () => {
  104. editor.setData( '<a href="url">foo</a>bar' );
  105. expect( getModelData( model, { withoutSelection: true } ) )
  106. .to.equal( '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  107. expect( editor.getData() ).to.equal( '<p><a href="url">foo</a>bar</p>' );
  108. } );
  109. // https://github.com/ckeditor/ckeditor5/issues/500
  110. it( 'should not pick up `<a name="foo">`', () => {
  111. editor.setData( '<p><a name="foo">foo</a>bar</p>' );
  112. expect( getModelData( model, { withoutSelection: true } ) )
  113. .to.equal( '<paragraph>foobar</paragraph>' );
  114. } );
  115. // CKEditor 4 does. And CKEditor 5's balloon allows creating such links.
  116. it( 'should pick up `<a href="">`', () => {
  117. editor.setData( '<p><a href="">foo</a>bar</p>' );
  118. expect( getModelData( model, { withoutSelection: true } ) )
  119. .to.equal( '<paragraph><$text linkHref="">foo</$text>bar</paragraph>' );
  120. expect( editor.getData() ).to.equal( '<p><a href="">foo</a>bar</p>' );
  121. } );
  122. // The editor's role is not to filter out potentially malicious data.
  123. // Its job is to not let this code be executed inside the editor (see the test in "editing pipeline conversion").
  124. it( 'should output a link with a potential XSS code', () => {
  125. setModelData( model, '<paragraph>[]</paragraph>' );
  126. model.change( writer => {
  127. writer.insertText( 'foo', { linkHref: 'javascript:alert(1)' }, model.document.selection.getFirstPosition() );
  128. } );
  129. expect( editor.getData() ).to.equal( '<p><a href="javascript:alert(1)">foo</a></p>' );
  130. } );
  131. it( 'should load a link with a potential XSS code', () => {
  132. editor.setData( '<p><a href="javascript:alert(1)">foo</a></p>' );
  133. expect( getModelData( model, { withoutSelection: true } ) )
  134. .to.equal( '<paragraph><$text linkHref="javascript:alert(1)">foo</$text></paragraph>' );
  135. } );
  136. } );
  137. describe( 'editing pipeline conversion', () => {
  138. it( 'should convert attribute', () => {
  139. setModelData( model, '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  140. expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '<p><a href="url">foo</a>bar</p>' );
  141. } );
  142. it( 'should convert to link element instance', () => {
  143. setModelData( model, '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  144. const element = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 0 );
  145. expect( isLinkElement( element ) ).to.be.true;
  146. } );
  147. // https://github.com/ckeditor/ckeditor5-link/issues/121
  148. it( 'should should set priority for `linkHref` higher than all other attribute elements', () => {
  149. model.schema.extend( '$text', { allowAttributes: 'foo' } );
  150. editor.conversion.for( 'downcast' ).attributeToElement( { model: 'foo', view: 'f' } );
  151. setModelData( model,
  152. '<paragraph>' +
  153. '<$text linkHref="url">a</$text><$text foo="true" linkHref="url">b</$text><$text linkHref="url">c</$text>' +
  154. '</paragraph>' );
  155. expect( editor.getData() ).to.equal( '<p><a href="url">a<f>b</f>c</a></p>' );
  156. } );
  157. it( 'must not render a link with a potential XSS code', () => {
  158. setModelData( model, '<paragraph><$text linkHref="javascript:alert(1)">[]foo</$text>bar[]</paragraph>' );
  159. expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
  160. .to.equal( '<p><a href="#">foo</a>bar</p>' );
  161. } );
  162. } );
  163. describe( 'link highlighting', () => {
  164. it( 'should convert the highlight to a proper view classes', () => {
  165. setModelData( model,
  166. '<paragraph>foo <$text linkHref="url">b{}ar</$text> baz</paragraph>'
  167. );
  168. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  169. expect( getViewData( view ) ).to.equal(
  170. '<p>foo <a class="ck-link_selected" href="url">b{}ar</a> baz</p>'
  171. );
  172. } );
  173. it( 'should work whenever selection has linkHref attribute - link start', () => {
  174. setModelData( model,
  175. '<paragraph>foo {}<$text linkHref="url">bar</$text> baz</paragraph>'
  176. );
  177. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.false;
  178. model.change( writer => {
  179. writer.setSelectionAttribute( 'linkHref', 'url' );
  180. } );
  181. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  182. expect( getViewData( view ) ).to.equal(
  183. '<p>foo <a class="ck-link_selected" href="url">{}bar</a> baz</p>'
  184. );
  185. } );
  186. it( 'should work whenever selection has linkHref attribute - link end', () => {
  187. setModelData( model,
  188. '<paragraph>foo <$text linkHref="url">bar</$text>{} baz</paragraph>'
  189. );
  190. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  191. expect( getViewData( view ) ).to.equal(
  192. '<p>foo <a class="ck-link_selected" href="url">bar{}</a> baz</p>'
  193. );
  194. } );
  195. it( 'should render highlight correctly after splitting the link', () => {
  196. setModelData( model,
  197. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  198. );
  199. editor.execute( 'enter' );
  200. expect( getModelData( model ) ).to.equal(
  201. '<paragraph>foo <$text linkHref="url">li</$text></paragraph>' +
  202. '<paragraph><$text linkHref="url">[]nk</$text> baz</paragraph>'
  203. );
  204. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  205. } );
  206. it( 'should remove classes when selection is moved out from the link', () => {
  207. setModelData( model,
  208. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  209. );
  210. expect( getViewData( view ) ).to.equal(
  211. '<p>foo <a class="ck-link_selected" href="url">li{}nk</a> baz</p>'
  212. );
  213. model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 0 ) );
  214. expect( getViewData( view ) ).to.equal(
  215. '<p>{}foo <a href="url">link</a> baz</p>'
  216. );
  217. } );
  218. it( 'should work correctly when selection is moved inside link', () => {
  219. setModelData( model,
  220. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  221. );
  222. expect( getViewData( view ) ).to.equal(
  223. '<p>foo <a class="ck-link_selected" href="url">li{}nk</a> baz</p>'
  224. );
  225. model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 5 ) );
  226. expect( getViewData( view ) ).to.equal(
  227. '<p>foo <a class="ck-link_selected" href="url">l{}ink</a> baz</p>'
  228. );
  229. } );
  230. describe( 'downcast conversion integration', () => {
  231. it( 'works for the #insert event', () => {
  232. setModelData( model,
  233. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  234. );
  235. model.change( writer => {
  236. writer.insertText( 'FOO', { linkHref: 'url' }, model.document.selection.getFirstPosition() );
  237. } );
  238. expect( getViewData( view ) ).to.equal(
  239. '<p>foo <a class="ck-link_selected" href="url">liFOO{}nk</a> baz</p>'
  240. );
  241. } );
  242. it( 'works for the #remove event', () => {
  243. setModelData( model,
  244. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  245. );
  246. model.change( writer => {
  247. writer.remove( writer.createRange(
  248. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
  249. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
  250. ) );
  251. } );
  252. expect( getViewData( view ) ).to.equal(
  253. '<p><a class="ck-link_selected" href="url">i{}nk</a> baz</p>'
  254. );
  255. } );
  256. it( 'works for the #attribute event', () => {
  257. setModelData( model,
  258. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  259. );
  260. model.change( writer => {
  261. writer.setAttribute( 'linkHref', 'new-url', writer.createRange(
  262. model.document.selection.getFirstPosition().getShiftedBy( -1 ),
  263. model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
  264. );
  265. } );
  266. expect( getViewData( view ) ).to.equal(
  267. '<p>foo <a href="url">l</a><a class="ck-link_selected" href="new-url">i{}n</a><a href="url">k</a> baz</p>'
  268. );
  269. } );
  270. it( 'works for the #selection event', () => {
  271. setModelData( model,
  272. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  273. );
  274. model.change( writer => {
  275. writer.setSelection( writer.createRange(
  276. model.document.selection.getFirstPosition().getShiftedBy( -1 ),
  277. model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
  278. );
  279. } );
  280. expect( getViewData( view ) ).to.equal(
  281. '<p>foo <a class="ck-link_selected" href="url">l{in}k</a> baz</p>'
  282. );
  283. } );
  284. it( 'works for the addMarker and removeMarker events', () => {
  285. editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'fooMarker', view: {} } );
  286. setModelData( model,
  287. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  288. );
  289. model.change( writer => {
  290. const range = writer.createRange(
  291. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
  292. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
  293. );
  294. writer.addMarker( 'fooMarker', { range, usingOperation: true } );
  295. } );
  296. expect( getViewData( view ) ).to.equal(
  297. '<p><span>foo </span><a class="ck-link_selected" href="url"><span>l</span>i{}nk</a> baz</p>'
  298. );
  299. model.change( writer => writer.removeMarker( 'fooMarker' ) );
  300. expect( getViewData( view ) ).to.equal(
  301. '<p>foo <a class="ck-link_selected" href="url">li{}nk</a> baz</p>'
  302. );
  303. } );
  304. } );
  305. } );
  306. describe( 'link attributes decorator', () => {
  307. describe( 'default behavior', () => {
  308. const testLinks = [
  309. {
  310. external: true,
  311. url: 'http://example.com'
  312. }, {
  313. external: true,
  314. url: 'https://cksource.com'
  315. }, {
  316. external: false,
  317. url: 'ftp://server.io'
  318. }, {
  319. external: true,
  320. url: '//schemaless.org'
  321. }, {
  322. external: false,
  323. url: 'www.ckeditor.com'
  324. }, {
  325. external: false,
  326. url: '/relative/url.html'
  327. }, {
  328. external: false,
  329. url: 'another/relative/url.html'
  330. }, {
  331. external: false,
  332. url: '#anchor'
  333. }, {
  334. external: false,
  335. url: 'mailto:some@user.org'
  336. }, {
  337. external: false,
  338. url: 'tel:123456789'
  339. }
  340. ];
  341. it( 'link.addTargetToExternalLinks is predefined as false value', () => {
  342. expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.false;
  343. } );
  344. describe( 'for link.addTargetToExternalLinks = false', () => {
  345. let editor, model;
  346. beforeEach( () => {
  347. return VirtualTestEditor
  348. .create( {
  349. plugins: [ Paragraph, LinkEditing, Enter ],
  350. link: {
  351. addTargetToExternalLinks: true
  352. }
  353. } )
  354. .then( newEditor => {
  355. editor = newEditor;
  356. model = editor.model;
  357. view = editor.editing.view;
  358. } );
  359. } );
  360. afterEach( () => {
  361. editor.destroy();
  362. } );
  363. it( 'link.addTargetToExternalLinks is set as true value', () => {
  364. expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.true;
  365. } );
  366. testLinks.forEach( link => {
  367. it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => {
  368. editor.setData( `<p><a href="${ link.url }">foo</a>bar</p>` );
  369. expect( getModelData( model, { withoutSelection: true } ) )
  370. .to.equal( `<paragraph><$text linkHref="${ link.url }">foo</$text>bar</paragraph>` );
  371. if ( link.external ) {
  372. expect( editor.getData() )
  373. .to.equal( `<p><a target="_blank" rel="noopener noreferrer" href="${ link.url }">foo</a>bar</p>` );
  374. } else {
  375. expect( editor.getData() ).to.equal( `<p><a href="${ link.url }">foo</a>bar</p>` );
  376. }
  377. } );
  378. } );
  379. } );
  380. testLinks.forEach( link => {
  381. it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => {
  382. editor.setData( `<p><a href="${ link.url }">foo</a>bar</p>` );
  383. expect( getModelData( model, { withoutSelection: true } ) )
  384. .to.equal( `<paragraph><$text linkHref="${ link.url }">foo</$text>bar</paragraph>` );
  385. expect( editor.getData() ).to.equal( `<p><a href="${ link.url }">foo</a>bar</p>` );
  386. } );
  387. } );
  388. } );
  389. describe( 'custom config', () => {
  390. describe( 'mode: automatic', () => {
  391. const testLinks = [
  392. {
  393. url: 'relative/url.html',
  394. attributes: {}
  395. }, {
  396. url: 'http://exmaple.com',
  397. attributes: {
  398. target: '_blank'
  399. }
  400. }, {
  401. url: 'https://example.com/download/link.pdf',
  402. attributes: {
  403. target: '_blank',
  404. download: 'download'
  405. }
  406. }, {
  407. url: 'mailto:some@person.io',
  408. attributes: {
  409. class: 'mail-url'
  410. }
  411. }
  412. ];
  413. beforeEach( () => {
  414. editor.destroy();
  415. return VirtualTestEditor
  416. .create( {
  417. plugins: [ Paragraph, LinkEditing, Enter ],
  418. link: {
  419. addTargetToExternalLinks: false,
  420. decorators: {
  421. isExternal: {
  422. mode: 'automatic',
  423. callback: url => url.startsWith( 'http' ),
  424. attributes: {
  425. target: '_blank'
  426. }
  427. },
  428. isDownloadable: {
  429. mode: 'automatic',
  430. callback: url => url.includes( 'download' ),
  431. attributes: {
  432. download: 'download'
  433. }
  434. },
  435. isMail: {
  436. mode: 'automatic',
  437. callback: url => url.startsWith( 'mailto:' ),
  438. attributes: {
  439. class: 'mail-url'
  440. }
  441. }
  442. }
  443. }
  444. } )
  445. .then( newEditor => {
  446. editor = newEditor;
  447. model = editor.model;
  448. view = editor.editing.view;
  449. } );
  450. } );
  451. testLinks.forEach( link => {
  452. it( `Link: ${ link.url } should get attributes: ${ JSON.stringify( link.attributes ) }`, () => {
  453. const ORDER = [ 'target', 'download', 'class' ];
  454. const attr = Object.entries( link.attributes ).sort( ( a, b ) => {
  455. const aIndex = ORDER.indexOf( a[ 0 ] );
  456. const bIndex = ORDER.indexOf( b[ 0 ] );
  457. return aIndex - bIndex;
  458. } );
  459. const reducedAttr = attr.reduce( ( acc, cur ) => {
  460. return acc + `${ cur[ 0 ] }="${ cur[ 1 ] }" `;
  461. }, '' );
  462. editor.setData( `<p><a href="${ link.url }">foo</a>bar</p>` );
  463. expect( getModelData( model, { withoutSelection: true } ) )
  464. .to.equal( `<paragraph><$text linkHref="${ link.url }">foo</$text>bar</paragraph>` );
  465. // Order of attributes is important, that's why this is assert is construct in such way.
  466. expect( editor.getData() ).to.equal( `<p><a ${ reducedAttr }href="${ link.url }">foo</a>bar</p>` );
  467. } );
  468. } );
  469. } );
  470. } );
  471. describe( 'custom linkHref converter', () => {
  472. beforeEach( () => {
  473. class CustomLinks extends Plugin {
  474. init() {
  475. const editor = this.editor;
  476. editor.conversion.for( 'downcast' ).add( dispatcher => {
  477. dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
  478. conversionApi.consumable.consume( data.item, 'attribute:linkHref' );
  479. // Very simplified downcast just for test assertion.
  480. const viewWriter = conversionApi.writer;
  481. const linkElement = viewWriter.createAttributeElement(
  482. 'a',
  483. {
  484. href: data.attributeNewValue
  485. }, {
  486. priority: 5
  487. }
  488. );
  489. viewWriter.setCustomProperty( 'link', true, linkElement );
  490. viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), linkElement );
  491. }, { priority: 'highest' } );
  492. } );
  493. }
  494. }
  495. editor.destroy();
  496. return VirtualTestEditor
  497. .create( {
  498. plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ],
  499. link: {
  500. addTargetToExternalLinks: true
  501. }
  502. } )
  503. .then( newEditor => {
  504. editor = newEditor;
  505. model = editor.model;
  506. view = editor.editing.view;
  507. } );
  508. } );
  509. it( 'has possibility to override default one', () => {
  510. editor.setData( '<p><a href="http://example.com">foo</a>bar</p>' );
  511. expect( getModelData( model, { withoutSelection: true } ) )
  512. .to.equal( '<paragraph><$text linkHref="http://example.com">foo</$text>bar</paragraph>' );
  513. expect( editor.getData() ).to.equal( '<p><a href="http://example.com">foo</a>bar</p>' );
  514. } );
  515. } );
  516. describe( 'upcast converter', () => {
  517. it( 'should upcast attributes from initial data', () => {
  518. return VirtualTestEditor
  519. .create( {
  520. initialData: '<p><a href="url" target="_blank" rel="noopener noreferrer" download="file">Foo</a>' +
  521. '<a href="example.com" download="file">Bar</a></p>',
  522. plugins: [ Paragraph, LinkEditing, Enter ],
  523. link: {
  524. decorators: {
  525. isExternal: {
  526. mode: 'manual',
  527. label: 'Open in a new window',
  528. attributes: {
  529. target: '_blank',
  530. rel: 'noopener noreferrer'
  531. }
  532. },
  533. isDownloadable: {
  534. mode: 'manual',
  535. label: 'Downloadable',
  536. attributes: {
  537. download: 'file'
  538. }
  539. }
  540. }
  541. }
  542. } )
  543. .then( newEditor => {
  544. editor = newEditor;
  545. model = editor.model;
  546. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  547. '<paragraph>' +
  548. '<$text linkHref="url" linkIsDownloadable="true" linkIsExternal="true">Foo</$text>' +
  549. '<$text linkHref="example.com" linkIsDownloadable="true">Bar</$text>' +
  550. '</paragraph>'
  551. );
  552. } );
  553. } );
  554. it( 'should upcast attributes from initial data for a manual decorator enabled by default', () => {
  555. return VirtualTestEditor
  556. .create( {
  557. initialData: '<p><a href="url" target="_blank" rel="noopener noreferrer" download="file">Foo</a>' +
  558. '<a href="example.com" download="file">Bar</a></p>',
  559. plugins: [ Paragraph, LinkEditing, Enter ],
  560. link: {
  561. decorators: {
  562. isExternal: {
  563. mode: 'manual',
  564. label: 'Open in a new window',
  565. attributes: {
  566. target: '_blank',
  567. rel: 'noopener noreferrer'
  568. },
  569. defaultValue: true
  570. },
  571. isDownloadable: {
  572. mode: 'manual',
  573. label: 'Downloadable',
  574. attributes: {
  575. download: 'file'
  576. }
  577. }
  578. }
  579. }
  580. } )
  581. .then( newEditor => {
  582. editor = newEditor;
  583. model = editor.model;
  584. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  585. '<paragraph>' +
  586. '<$text linkHref="url" linkIsDownloadable="true" linkIsExternal="true">Foo</$text>' +
  587. '<$text linkHref="example.com" linkIsDownloadable="true" linkIsExternal="false">Bar</$text>' +
  588. '</paragraph>'
  589. );
  590. } );
  591. } );
  592. it( 'should not upcast partial and incorrect attributes', () => {
  593. return VirtualTestEditor
  594. .create( {
  595. initialData: '<p><a href="url" target="_blank" download="something">Foo</a>' +
  596. '<a href="example.com" download="test">Bar</a></p>',
  597. plugins: [ Paragraph, LinkEditing, Enter ],
  598. link: {
  599. decorators: {
  600. isExternal: {
  601. mode: 'manual',
  602. label: 'Open in a new window',
  603. attributes: {
  604. target: '_blank',
  605. rel: 'noopener noreferrer'
  606. }
  607. },
  608. isDownloadable: {
  609. mode: 'manual',
  610. label: 'Downloadable',
  611. attributes: {
  612. download: 'file'
  613. }
  614. }
  615. }
  616. }
  617. } )
  618. .then( newEditor => {
  619. editor = newEditor;
  620. model = editor.model;
  621. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  622. '<paragraph>' +
  623. '<$text linkHref="url">Foo</$text>' +
  624. '<$text linkHref="example.com">Bar</$text>' +
  625. '</paragraph>'
  626. );
  627. } );
  628. } );
  629. } );
  630. } );
  631. } );