linkediting.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399
  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 ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  9. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  10. import BoldEditing from '@ckeditor/ckeditor5-basic-styles/src/bold/boldediting';
  11. import ItalicEditing from '@ckeditor/ckeditor5-basic-styles/src/italic/italicediting';
  12. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  13. import Enter from '@ckeditor/ckeditor5-enter/src/enter';
  14. import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  15. import ImageEditing from '@ckeditor/ckeditor5-image/src/image/imageediting';
  16. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  17. import Input from '@ckeditor/ckeditor5-typing/src/input';
  18. import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  19. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  20. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  21. import { isLinkElement } from '../src/utils';
  22. /* global document */
  23. describe( 'LinkEditing', () => {
  24. let element, editor, model, view;
  25. beforeEach( async () => {
  26. element = document.createElement( 'div' );
  27. document.body.appendChild( element );
  28. editor = await ClassicTestEditor.create( element, {
  29. plugins: [ Paragraph, LinkEditing, Enter ],
  30. link: {
  31. decorators: {
  32. isExternal: {
  33. mode: 'manual',
  34. label: 'Open in a new window',
  35. attributes: {
  36. target: '_blank',
  37. rel: 'noopener noreferrer'
  38. }
  39. }
  40. }
  41. }
  42. } );
  43. editor.model.schema.extend( '$text', { allowAttributes: 'bold' } );
  44. editor.conversion.attributeToElement( {
  45. model: 'bold',
  46. view: 'b'
  47. } );
  48. model = editor.model;
  49. view = editor.editing.view;
  50. } );
  51. afterEach( async () => {
  52. element.remove();
  53. await editor.destroy();
  54. } );
  55. it( 'should have pluginName', () => {
  56. expect( LinkEditing.pluginName ).to.equal( 'LinkEditing' );
  57. } );
  58. it( 'should be loaded', () => {
  59. expect( editor.plugins.get( LinkEditing ) ).to.be.instanceOf( LinkEditing );
  60. } );
  61. it( 'should set proper schema rules', () => {
  62. expect( model.schema.checkAttribute( [ '$block', '$text' ], 'linkHref' ) ).to.be.true;
  63. expect( model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'linkHref' ) ).to.be.true;
  64. expect( model.schema.checkAttribute( [ '$block' ], 'linkHref' ) ).to.be.false;
  65. } );
  66. // Let's check only the minimum to not duplicate `TwoStepCaretMovement` tests.
  67. // Testing minimum is better than testing using spies that might give false positive results.
  68. describe( 'two-step caret movement', () => {
  69. it( 'should be bound to the `linkHref` attribute (LTR)', () => {
  70. const selection = editor.model.document.selection;
  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 should read attributes from the left.
  74. expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.false;
  75. // So let's simulate the `keydown` event.
  76. editor.editing.view.document.fire( 'keydown', {
  77. keyCode: keyCodes.arrowright,
  78. preventDefault: () => {},
  79. domTarget: document.body
  80. } );
  81. expect( getModelData( model ) ).to.equal( '<paragraph>foo<$text linkHref="url">[]b</$text>ar</paragraph>' );
  82. // Selection should get the attributes from the right.
  83. expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.true;
  84. expect( selection.getAttribute( 'linkHref' ), 'linkHref attribute' ).to.equal( 'url' );
  85. } );
  86. it( 'should be bound to the `linkHref` attribute (RTL)', async () => {
  87. const editor = await ClassicTestEditor.create( element, {
  88. plugins: [ Paragraph, LinkEditing, Enter ],
  89. language: {
  90. content: 'ar'
  91. }
  92. } );
  93. model = editor.model;
  94. view = editor.editing.view;
  95. const selection = editor.model.document.selection;
  96. // Put selection before the link element.
  97. setModelData( editor.model, '<paragraph>foo[]<$text linkHref="url">b</$text>ar</paragraph>' );
  98. // The selection's gravity should read attributes from the left.
  99. expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.false;
  100. // So let's simulate the `keydown` event.
  101. editor.editing.view.document.fire( 'keydown', {
  102. keyCode: keyCodes.arrowleft,
  103. preventDefault: () => {},
  104. domTarget: document.body
  105. } );
  106. expect( getModelData( model ) ).to.equal( '<paragraph>foo<$text linkHref="url">[]b</$text>ar</paragraph>' );
  107. // Selection should get the attributes from the right.
  108. expect( selection.hasAttribute( 'linkHref' ), 'hasAttribute( \'linkHref\' )' ).to.be.true;
  109. expect( selection.getAttribute( 'linkHref' ), 'linkHref attribute' ).to.equal( 'url' );
  110. await editor.destroy();
  111. } );
  112. } );
  113. // https://github.com/ckeditor/ckeditor5/issues/6053
  114. describe( 'selection attribute management on paste', () => {
  115. it( 'should remove link atttributes when pasting a link', () => {
  116. setModelData( model, '<paragraph>foo[]</paragraph>' );
  117. model.change( writer => {
  118. model.insertContent( writer.createText( 'INSERTED', { linkHref: 'ckeditor.com' } ) );
  119. } );
  120. expect( getModelData( model ) ).to.equal( '<paragraph>foo<$text linkHref="ckeditor.com">INSERTED</$text>[]</paragraph>' );
  121. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.be.empty;
  122. } );
  123. it( 'should remove all atttributes starting with "link" (e.g. decorator attributes) when pasting a link', () => {
  124. setModelData( model, '<paragraph>foo[]</paragraph>' );
  125. model.change( writer => {
  126. model.insertContent( writer.createText( 'INSERTED', { linkHref: 'ckeditor.com', linkIsExternal: true } ) );
  127. } );
  128. expect( getModelData( model ) ).to.equal(
  129. '<paragraph>' +
  130. 'foo<$text linkHref="ckeditor.com" linkIsExternal="true">INSERTED</$text>[]' +
  131. '</paragraph>'
  132. );
  133. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.be.empty;
  134. } );
  135. it( 'should not remove link atttributes when pasting a non-link content', () => {
  136. setModelData( model, '<paragraph><$text linkHref="ckeditor.com">foo[]</$text></paragraph>' );
  137. model.change( writer => {
  138. model.insertContent( writer.createText( 'INSERTED', { bold: 'true' } ) );
  139. } );
  140. expect( getModelData( model ) ).to.equal(
  141. '<paragraph>' +
  142. '<$text linkHref="ckeditor.com">foo</$text>' +
  143. '<$text bold="true">INSERTED[]</$text>' +
  144. '</paragraph>'
  145. );
  146. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'bold' ] );
  147. } );
  148. it( 'should not remove link atttributes when pasting in the middle of a link with the same URL', () => {
  149. setModelData( model, '<paragraph><$text linkHref="ckeditor.com">fo[]o</$text></paragraph>' );
  150. model.change( writer => {
  151. model.insertContent( writer.createText( 'INSERTED', { linkHref: 'ckeditor.com' } ) );
  152. } );
  153. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="ckeditor.com">foINSERTED[]o</$text></paragraph>' );
  154. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
  155. } );
  156. it( 'should not remove link atttributes from the selection when pasting before a link when the gravity is overridden', () => {
  157. setModelData( model, '<paragraph>foo[]<$text linkHref="ckeditor.com">bar</$text></paragraph>' );
  158. view.document.fire( 'keydown', {
  159. keyCode: keyCodes.arrowright,
  160. preventDefault: () => {},
  161. domTarget: document.body
  162. } );
  163. expect( model.document.selection.isGravityOverridden ).to.be.true;
  164. model.change( writer => {
  165. model.insertContent( writer.createText( 'INSERTED', { bold: true } ) );
  166. } );
  167. expect( getModelData( model ) ).to.equal(
  168. '<paragraph>' +
  169. 'foo' +
  170. '<$text bold="true">INSERTED</$text>' +
  171. '<$text linkHref="ckeditor.com">[]bar</$text>' +
  172. '</paragraph>'
  173. );
  174. expect( model.document.selection.isGravityOverridden ).to.be.true;
  175. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
  176. } );
  177. it( 'should not remove link atttributes when pasting a link into another link (different URLs, no merge)', () => {
  178. setModelData( model, '<paragraph><$text linkHref="ckeditor.com">f[]oo</$text></paragraph>' );
  179. model.change( writer => {
  180. model.insertContent( writer.createText( 'INSERTED', { linkHref: 'http://INSERTED' } ) );
  181. } );
  182. expect( getModelData( model ) ).to.equal(
  183. '<paragraph>' +
  184. '<$text linkHref="ckeditor.com">f</$text>' +
  185. '<$text linkHref="http://INSERTED">INSERTED[]</$text>' +
  186. '<$text linkHref="ckeditor.com">oo</$text>' +
  187. '</paragraph>'
  188. );
  189. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
  190. } );
  191. it( 'should not remove link atttributes when pasting before another link (different URLs, no merge)', () => {
  192. setModelData( model, '<paragraph>[]<$text linkHref="ckeditor.com">foo</$text></paragraph>' );
  193. expect( model.document.selection.isGravityOverridden ).to.be.false;
  194. model.change( writer => {
  195. model.insertContent( writer.createText( 'INSERTED', { linkHref: 'http://INSERTED' } ) );
  196. } );
  197. expect( getModelData( model ) ).to.equal(
  198. '<paragraph>' +
  199. '<$text linkHref="http://INSERTED">INSERTED[]</$text>' +
  200. '<$text linkHref="ckeditor.com">foo</$text>' +
  201. '</paragraph>'
  202. );
  203. expect( [ ...model.document.selection.getAttributeKeys() ] ).to.have.members( [ 'linkHref' ] );
  204. expect( model.document.selection.getAttribute( 'linkHref' ) ).to.equal( 'http://INSERTED' );
  205. } );
  206. } );
  207. describe( 'command', () => {
  208. it( 'should register link command', () => {
  209. const command = editor.commands.get( 'link' );
  210. expect( command ).to.be.instanceOf( LinkCommand );
  211. } );
  212. it( 'should register unlink command', () => {
  213. const command = editor.commands.get( 'unlink' );
  214. expect( command ).to.be.instanceOf( UnlinkCommand );
  215. } );
  216. } );
  217. describe( 'data pipeline conversions', () => {
  218. it( 'should convert `<a href="url">` to `linkHref="url"` attribute', () => {
  219. editor.setData( '<p><a href="url">foo</a>bar</p>' );
  220. expect( getModelData( model, { withoutSelection: true } ) )
  221. .to.equal( '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  222. expect( editor.getData() ).to.equal( '<p><a href="url">foo</a>bar</p>' );
  223. } );
  224. it( 'should be integrated with autoparagraphing', () => {
  225. editor.setData( '<a href="url">foo</a>bar' );
  226. expect( getModelData( model, { withoutSelection: true } ) )
  227. .to.equal( '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  228. expect( editor.getData() ).to.equal( '<p><a href="url">foo</a>bar</p>' );
  229. } );
  230. // https://github.com/ckeditor/ckeditor5/issues/500
  231. it( 'should not pick up `<a name="foo">`', () => {
  232. editor.setData( '<p><a name="foo">foo</a>bar</p>' );
  233. expect( getModelData( model, { withoutSelection: true } ) )
  234. .to.equal( '<paragraph>foobar</paragraph>' );
  235. } );
  236. // CKEditor 4 does. And CKEditor 5's balloon allows creating such links.
  237. it( 'should pick up `<a href="">`', () => {
  238. editor.setData( '<p><a href="">foo</a>bar</p>' );
  239. expect( getModelData( model, { withoutSelection: true } ) )
  240. .to.equal( '<paragraph><$text linkHref="">foo</$text>bar</paragraph>' );
  241. expect( editor.getData() ).to.equal( '<p><a href="">foo</a>bar</p>' );
  242. } );
  243. // The editor's role is not to filter out potentially malicious data.
  244. // Its job is to not let this code be executed inside the editor (see the test in "editing pipeline conversion").
  245. it( 'should output a link with a potential XSS code', () => {
  246. setModelData( model, '<paragraph>[]</paragraph>' );
  247. model.change( writer => {
  248. writer.insertText( 'foo', { linkHref: 'javascript:alert(1)' }, model.document.selection.getFirstPosition() );
  249. } );
  250. expect( editor.getData() ).to.equal( '<p><a href="javascript:alert(1)">foo</a></p>' );
  251. } );
  252. it( 'should load a link with a potential XSS code', () => {
  253. editor.setData( '<p><a href="javascript:alert(1)">foo</a></p>' );
  254. expect( getModelData( model, { withoutSelection: true } ) )
  255. .to.equal( '<paragraph><$text linkHref="javascript:alert(1)">foo</$text></paragraph>' );
  256. } );
  257. } );
  258. describe( 'editing pipeline conversion', () => {
  259. it( 'should convert attribute', () => {
  260. setModelData( model, '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  261. expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '<p><a href="url">foo</a>bar</p>' );
  262. } );
  263. it( 'should convert to link element instance', () => {
  264. setModelData( model, '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' );
  265. const element = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 0 );
  266. expect( isLinkElement( element ) ).to.be.true;
  267. } );
  268. // https://github.com/ckeditor/ckeditor5-link/issues/121
  269. it( 'should should set priority for `linkHref` higher than all other attribute elements', () => {
  270. model.schema.extend( '$text', { allowAttributes: 'foo' } );
  271. editor.conversion.for( 'downcast' ).attributeToElement( { model: 'foo', view: 'f' } );
  272. setModelData( model,
  273. '<paragraph>' +
  274. '<$text linkHref="url">a</$text><$text foo="true" linkHref="url">b</$text><$text linkHref="url">c</$text>' +
  275. '</paragraph>' );
  276. expect( editor.getData() ).to.equal( '<p><a href="url">a<f>b</f>c</a></p>' );
  277. } );
  278. it( 'must not render a link with a potential XSS code', () => {
  279. setModelData( model, '<paragraph><$text linkHref="javascript:alert(1)">[]foo</$text>bar[]</paragraph>' );
  280. expect( getViewData( editor.editing.view, { withoutSelection: true } ) )
  281. .to.equal( '<p><a href="#">foo</a>bar</p>' );
  282. } );
  283. } );
  284. describe( 'link highlighting', () => {
  285. it( 'should convert the highlight to a proper view classes', () => {
  286. setModelData( model,
  287. '<paragraph>foo <$text linkHref="url">b{}ar</$text> baz</paragraph>'
  288. );
  289. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  290. expect( getViewData( view ) ).to.equal(
  291. '<p>foo <a class="ck-link_selected" href="url">b{}ar</a> baz</p>'
  292. );
  293. } );
  294. it( 'should work whenever selection has linkHref attribute - link start', () => {
  295. setModelData( model,
  296. '<paragraph>foo {}<$text linkHref="url">bar</$text> baz</paragraph>'
  297. );
  298. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.false;
  299. model.change( writer => {
  300. writer.setSelectionAttribute( 'linkHref', 'url' );
  301. } );
  302. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  303. expect( getViewData( view ) ).to.equal(
  304. '<p>foo <a class="ck-link_selected" href="url">{}bar</a> baz</p>'
  305. );
  306. } );
  307. it( 'should work whenever selection has linkHref attribute - link end', () => {
  308. setModelData( model,
  309. '<paragraph>foo <$text linkHref="url">bar</$text>{} baz</paragraph>'
  310. );
  311. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  312. expect( getViewData( view ) ).to.equal(
  313. '<p>foo <a class="ck-link_selected" href="url">bar{}</a> baz</p>'
  314. );
  315. } );
  316. it( 'should render highlight correctly after splitting the link', () => {
  317. setModelData( model,
  318. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  319. );
  320. editor.execute( 'enter' );
  321. expect( getModelData( model ) ).to.equal(
  322. '<paragraph>foo <$text linkHref="url">li</$text></paragraph>' +
  323. '<paragraph><$text linkHref="url">[]nk</$text> baz</paragraph>'
  324. );
  325. expect( model.document.selection.hasAttribute( 'linkHref' ) ).to.be.true;
  326. } );
  327. it( 'should remove classes when selection is moved out from the link', () => {
  328. setModelData( model,
  329. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  330. );
  331. expect( getViewData( view ) ).to.equal(
  332. '<p>foo <a class="ck-link_selected" href="url">li{}nk</a> baz</p>'
  333. );
  334. model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 0 ) );
  335. expect( getViewData( view ) ).to.equal(
  336. '<p>{}foo <a href="url">link</a> baz</p>'
  337. );
  338. } );
  339. it( 'should work correctly when selection is moved inside link', () => {
  340. setModelData( model,
  341. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  342. );
  343. expect( getViewData( view ) ).to.equal(
  344. '<p>foo <a class="ck-link_selected" href="url">li{}nk</a> baz</p>'
  345. );
  346. model.change( writer => writer.setSelection( model.document.getRoot().getChild( 0 ), 5 ) );
  347. expect( getViewData( view ) ).to.equal(
  348. '<p>foo <a class="ck-link_selected" href="url">l{}ink</a> baz</p>'
  349. );
  350. } );
  351. describe( 'downcast conversion integration', () => {
  352. it( 'works for the #insert event', () => {
  353. setModelData( model,
  354. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  355. );
  356. model.change( writer => {
  357. writer.insertText( 'FOO', { linkHref: 'url' }, model.document.selection.getFirstPosition() );
  358. } );
  359. expect( getViewData( view ) ).to.equal(
  360. '<p>foo <a class="ck-link_selected" href="url">liFOO{}nk</a> baz</p>'
  361. );
  362. } );
  363. it( 'works for the #remove event', () => {
  364. setModelData( model,
  365. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  366. );
  367. model.change( writer => {
  368. writer.remove( writer.createRange(
  369. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
  370. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
  371. ) );
  372. } );
  373. expect( getViewData( view ) ).to.equal(
  374. '<p><a class="ck-link_selected" href="url">i{}nk</a> baz</p>'
  375. );
  376. } );
  377. it( 'works for the #attribute event', () => {
  378. setModelData( model,
  379. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  380. );
  381. model.change( writer => {
  382. writer.setAttribute( 'linkHref', 'new-url', writer.createRange(
  383. model.document.selection.getFirstPosition().getShiftedBy( -1 ),
  384. model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
  385. );
  386. } );
  387. expect( getViewData( view ) ).to.equal(
  388. '<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>'
  389. );
  390. } );
  391. it( 'works for the #selection event', () => {
  392. setModelData( model,
  393. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  394. );
  395. model.change( writer => {
  396. writer.setSelection( writer.createRange(
  397. model.document.selection.getFirstPosition().getShiftedBy( -1 ),
  398. model.document.selection.getFirstPosition().getShiftedBy( 1 ) )
  399. );
  400. } );
  401. expect( getViewData( view ) ).to.equal(
  402. '<p>foo <a class="ck-link_selected" href="url">l{in}k</a> baz</p>'
  403. );
  404. } );
  405. it( 'works for the addMarker and removeMarker events', () => {
  406. editor.conversion.for( 'editingDowncast' ).markerToHighlight( { model: 'fooMarker', view: {} } );
  407. setModelData( model,
  408. '<paragraph>foo <$text linkHref="url">li{}nk</$text> baz</paragraph>'
  409. );
  410. model.change( writer => {
  411. const range = writer.createRange(
  412. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 0 ),
  413. writer.createPositionAt( model.document.getRoot().getChild( 0 ), 5 )
  414. );
  415. writer.addMarker( 'fooMarker', { range, usingOperation: true } );
  416. } );
  417. expect( getViewData( view ) ).to.equal(
  418. '<p><span>foo </span><a class="ck-link_selected" href="url"><span>l</span>i{}nk</a> baz</p>'
  419. );
  420. model.change( writer => writer.removeMarker( 'fooMarker' ) );
  421. expect( getViewData( view ) ).to.equal(
  422. '<p>foo <a class="ck-link_selected" href="url">li{}nk</a> baz</p>'
  423. );
  424. } );
  425. } );
  426. } );
  427. describe( 'link attributes decorator', () => {
  428. describe( 'default behavior', () => {
  429. const testLinks = [
  430. {
  431. external: true,
  432. url: 'http://example.com'
  433. }, {
  434. external: true,
  435. url: 'https://cksource.com'
  436. }, {
  437. external: false,
  438. url: 'ftp://server.io'
  439. }, {
  440. external: true,
  441. url: '//schemaless.org'
  442. }, {
  443. external: false,
  444. url: 'www.ckeditor.com'
  445. }, {
  446. external: false,
  447. url: '/relative/url.html'
  448. }, {
  449. external: false,
  450. url: 'another/relative/url.html'
  451. }, {
  452. external: false,
  453. url: '#anchor'
  454. }, {
  455. external: false,
  456. url: 'mailto:some@user.org'
  457. }, {
  458. external: false,
  459. url: 'tel:123456789'
  460. }
  461. ];
  462. it( 'link.addTargetToExternalLinks is predefined as false value', () => {
  463. expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.false;
  464. } );
  465. describe( 'for link.addTargetToExternalLinks = false', () => {
  466. let editor, model;
  467. beforeEach( async () => {
  468. editor = await ClassicTestEditor.create( element, {
  469. plugins: [ Paragraph, LinkEditing, Enter ],
  470. link: {
  471. addTargetToExternalLinks: true
  472. }
  473. } );
  474. model = editor.model;
  475. view = editor.editing.view;
  476. } );
  477. afterEach( async () => {
  478. await editor.destroy();
  479. } );
  480. it( 'link.addTargetToExternalLinks is set as true value', () => {
  481. expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.true;
  482. } );
  483. testLinks.forEach( link => {
  484. it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => {
  485. editor.setData( `<p><a href="${ link.url }">foo</a>bar</p>` );
  486. expect( getModelData( model, { withoutSelection: true } ) )
  487. .to.equal( `<paragraph><$text linkHref="${ link.url }">foo</$text>bar</paragraph>` );
  488. if ( link.external ) {
  489. expect( editor.getData() )
  490. .to.equal( `<p><a target="_blank" rel="noopener noreferrer" href="${ link.url }">foo</a>bar</p>` );
  491. } else {
  492. expect( editor.getData() ).to.equal( `<p><a href="${ link.url }">foo</a>bar</p>` );
  493. }
  494. } );
  495. } );
  496. } );
  497. testLinks.forEach( link => {
  498. it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => {
  499. editor.setData( `<p><a href="${ link.url }">foo</a>bar</p>` );
  500. expect( getModelData( model, { withoutSelection: true } ) )
  501. .to.equal( `<paragraph><$text linkHref="${ link.url }">foo</$text>bar</paragraph>` );
  502. expect( editor.getData() ).to.equal( `<p><a href="${ link.url }">foo</a>bar</p>` );
  503. } );
  504. } );
  505. } );
  506. describe( 'custom config', () => {
  507. describe( 'mode: automatic', () => {
  508. const testLinks = [
  509. {
  510. url: 'relative/url.html',
  511. attributes: {}
  512. }, {
  513. url: 'http://exmaple.com',
  514. attributes: {
  515. target: '_blank'
  516. }
  517. }, {
  518. url: 'https://example.com/download/link.pdf',
  519. attributes: {
  520. target: '_blank',
  521. download: 'download'
  522. }
  523. }, {
  524. url: 'mailto:some@person.io',
  525. attributes: {
  526. class: 'mail-url'
  527. }
  528. }
  529. ];
  530. beforeEach( async () => {
  531. await editor.destroy();
  532. editor = await ClassicTestEditor.create( element, {
  533. plugins: [ Paragraph, LinkEditing, Enter ],
  534. link: {
  535. addTargetToExternalLinks: false,
  536. decorators: {
  537. isExternal: {
  538. mode: 'automatic',
  539. callback: url => url.startsWith( 'http' ),
  540. attributes: {
  541. target: '_blank'
  542. }
  543. },
  544. isDownloadable: {
  545. mode: 'automatic',
  546. callback: url => url.includes( 'download' ),
  547. attributes: {
  548. download: 'download'
  549. }
  550. },
  551. isMail: {
  552. mode: 'automatic',
  553. callback: url => url.startsWith( 'mailto:' ),
  554. attributes: {
  555. class: 'mail-url'
  556. }
  557. }
  558. }
  559. }
  560. } );
  561. model = editor.model;
  562. view = editor.editing.view;
  563. } );
  564. testLinks.forEach( link => {
  565. it( `Link: ${ link.url } should get attributes: ${ JSON.stringify( link.attributes ) }`, () => {
  566. const ORDER = [ 'target', 'download', 'class' ];
  567. const attr = Object.entries( link.attributes ).sort( ( a, b ) => {
  568. const aIndex = ORDER.indexOf( a[ 0 ] );
  569. const bIndex = ORDER.indexOf( b[ 0 ] );
  570. return aIndex - bIndex;
  571. } );
  572. const reducedAttr = attr.reduce( ( acc, cur ) => {
  573. return acc + `${ cur[ 0 ] }="${ cur[ 1 ] }" `;
  574. }, '' );
  575. editor.setData( `<p><a href="${ link.url }">foo</a>bar</p>` );
  576. expect( getModelData( model, { withoutSelection: true } ) )
  577. .to.equal( `<paragraph><$text linkHref="${ link.url }">foo</$text>bar</paragraph>` );
  578. // Order of attributes is important, that's why this is assert is construct in such way.
  579. expect( editor.getData() ).to.equal( `<p><a ${ reducedAttr }href="${ link.url }">foo</a>bar</p>` );
  580. } );
  581. } );
  582. } );
  583. } );
  584. describe( 'custom linkHref converter', () => {
  585. beforeEach( async () => {
  586. class CustomLinks extends Plugin {
  587. init() {
  588. const editor = this.editor;
  589. editor.conversion.for( 'downcast' ).add( dispatcher => {
  590. dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
  591. conversionApi.consumable.consume( data.item, 'attribute:linkHref' );
  592. // Very simplified downcast just for test assertion.
  593. const viewWriter = conversionApi.writer;
  594. const linkElement = viewWriter.createAttributeElement(
  595. 'a',
  596. {
  597. href: data.attributeNewValue
  598. }, {
  599. priority: 5
  600. }
  601. );
  602. viewWriter.setCustomProperty( 'link', true, linkElement );
  603. viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), linkElement );
  604. }, { priority: 'highest' } );
  605. } );
  606. }
  607. }
  608. await editor.destroy();
  609. editor = await ClassicTestEditor.create( element, {
  610. plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ],
  611. link: {
  612. addTargetToExternalLinks: true
  613. }
  614. } );
  615. model = editor.model;
  616. view = editor.editing.view;
  617. } );
  618. it( 'has possibility to override default one', () => {
  619. editor.setData( '<p><a href="http://example.com">foo</a>bar</p>' );
  620. expect( getModelData( model, { withoutSelection: true } ) )
  621. .to.equal( '<paragraph><$text linkHref="http://example.com">foo</$text>bar</paragraph>' );
  622. expect( editor.getData() ).to.equal( '<p><a href="http://example.com">foo</a>bar</p>' );
  623. } );
  624. } );
  625. describe( 'upcast converter', () => {
  626. let element, editor;
  627. beforeEach( () => {
  628. element = document.createElement( 'div' );
  629. document.body.appendChild( element );
  630. } );
  631. afterEach( () => {
  632. element.remove();
  633. } );
  634. it( 'should upcast attributes from initial data', async () => {
  635. editor = await ClassicTestEditor.create( element, {
  636. initialData: '<p><a href="url" target="_blank" rel="noopener noreferrer" download="file">Foo</a>' +
  637. '<a href="example.com" download="file">Bar</a></p>',
  638. plugins: [ Paragraph, LinkEditing, Enter ],
  639. link: {
  640. decorators: {
  641. isExternal: {
  642. mode: 'manual',
  643. label: 'Open in a new window',
  644. attributes: {
  645. target: '_blank',
  646. rel: 'noopener noreferrer'
  647. }
  648. },
  649. isDownloadable: {
  650. mode: 'manual',
  651. label: 'Downloadable',
  652. attributes: {
  653. download: 'file'
  654. }
  655. }
  656. }
  657. }
  658. } );
  659. model = editor.model;
  660. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  661. '<paragraph>' +
  662. '<$text linkHref="url" linkIsDownloadable="true" linkIsExternal="true">Foo</$text>' +
  663. '<$text linkHref="example.com" linkIsDownloadable="true">Bar</$text>' +
  664. '</paragraph>'
  665. );
  666. await editor.destroy();
  667. } );
  668. it( 'should not upcast partial and incorrect attributes', async () => {
  669. editor = await ClassicTestEditor.create( element, {
  670. initialData: '<p><a href="url" target="_blank" download="something">Foo</a>' +
  671. '<a href="example.com" download="test">Bar</a></p>',
  672. plugins: [ Paragraph, LinkEditing, Enter ],
  673. link: {
  674. decorators: {
  675. isExternal: {
  676. mode: 'manual',
  677. label: 'Open in a new window',
  678. attributes: {
  679. target: '_blank',
  680. rel: 'noopener noreferrer'
  681. }
  682. },
  683. isDownloadable: {
  684. mode: 'manual',
  685. label: 'Downloadable',
  686. attributes: {
  687. download: 'file'
  688. }
  689. }
  690. }
  691. }
  692. } );
  693. model = editor.model;
  694. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  695. '<paragraph>' +
  696. '<$text linkHref="url">Foo</$text>' +
  697. '<$text linkHref="example.com">Bar</$text>' +
  698. '</paragraph>'
  699. );
  700. await editor.destroy();
  701. } );
  702. } );
  703. } );
  704. // https://github.com/ckeditor/ckeditor5/issues/1016
  705. describe( 'typing around the link after a click', () => {
  706. let editor;
  707. beforeEach( async () => {
  708. editor = await ClassicTestEditor.create( element, {
  709. plugins: [ Paragraph, LinkEditing, Enter, Input, BoldEditing ],
  710. link: {
  711. decorators: {
  712. isFoo: {
  713. mode: 'manual',
  714. label: 'Foo',
  715. attributes: {
  716. class: 'foo'
  717. }
  718. },
  719. isBar: {
  720. mode: 'manual',
  721. label: 'Bar',
  722. attributes: {
  723. target: '_blank'
  724. }
  725. }
  726. }
  727. }
  728. } );
  729. model = editor.model;
  730. view = editor.editing.view;
  731. model.schema.extend( '$text', {
  732. allowIn: '$root',
  733. allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
  734. } );
  735. } );
  736. afterEach( async () => {
  737. await editor.destroy();
  738. } );
  739. it( 'should insert content after the link', () => {
  740. setModelData( model, '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
  741. editor.editing.view.document.fire( 'mousedown' );
  742. editor.editing.view.document.fire( 'selectionChange', {
  743. newSelection: view.document.selection
  744. } );
  745. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar</$text>[]</paragraph>' );
  746. editor.execute( 'input', { text: 'Foo' } );
  747. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar</$text>Foo[]</paragraph>' );
  748. } );
  749. it( 'should insert content before the link', () => {
  750. setModelData( model, '<paragraph><$text linkHref="url">[]Bar</$text></paragraph>' );
  751. editor.editing.view.document.fire( 'mousedown' );
  752. editor.editing.view.document.fire( 'selectionChange', {
  753. newSelection: view.document.selection
  754. } );
  755. expect( getModelData( model ) ).to.equal( '<paragraph>[]<$text linkHref="url">Bar</$text></paragraph>' );
  756. editor.execute( 'input', { text: 'Foo' } );
  757. expect( getModelData( model ) ).to.equal( '<paragraph>Foo[]<$text linkHref="url">Bar</$text></paragraph>' );
  758. } );
  759. it( 'should insert content to the link if clicked inside it', () => {
  760. setModelData( model, '<paragraph><$text linkHref="url">B[]ar</$text></paragraph>' );
  761. editor.editing.view.document.fire( 'mousedown' );
  762. editor.editing.view.document.fire( 'selectionChange', {
  763. newSelection: view.document.selection
  764. } );
  765. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">B[]ar</$text></paragraph>' );
  766. editor.execute( 'input', { text: 'ar. B' } );
  767. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar. B[]ar</$text></paragraph>' );
  768. } );
  769. it( 'should insert content between two links (selection at the end of the first link)', () => {
  770. setModelData( model, '<paragraph><$text linkHref="foo">Foo[]</$text><$text linkHref="bar">Bar</$text></paragraph>' );
  771. editor.editing.view.document.fire( 'mousedown' );
  772. editor.editing.view.document.fire( 'selectionChange', {
  773. newSelection: view.document.selection
  774. } );
  775. expect( getModelData( model ) ).to.equal(
  776. '<paragraph><$text linkHref="foo">Foo</$text>[]<$text linkHref="bar">Bar</$text></paragraph>'
  777. );
  778. editor.execute( 'input', { text: 'Foo' } );
  779. expect( getModelData( model ) ).to.equal(
  780. '<paragraph><$text linkHref="foo">Foo</$text>Foo[]<$text linkHref="bar">Bar</$text></paragraph>'
  781. );
  782. } );
  783. it( 'should insert content between two links (selection at the beginning of the second link)', () => {
  784. setModelData( model, '<paragraph><$text linkHref="foo">Foo</$text><$text linkHref="bar">[]Bar</$text></paragraph>' );
  785. editor.editing.view.document.fire( 'mousedown' );
  786. editor.editing.view.document.fire( 'selectionChange', {
  787. newSelection: view.document.selection
  788. } );
  789. expect( getModelData( model ) ).to.equal(
  790. '<paragraph><$text linkHref="foo">Foo</$text>[]<$text linkHref="bar">Bar</$text></paragraph>'
  791. );
  792. editor.execute( 'input', { text: 'Foo' } );
  793. expect( getModelData( model ) ).to.equal(
  794. '<paragraph><$text linkHref="foo">Foo</$text>Foo[]<$text linkHref="bar">Bar</$text></paragraph>'
  795. );
  796. } );
  797. it( 'should not touch other attributes than `linkHref`', () => {
  798. setModelData( model, '<paragraph><$text bold="true" linkHref="url">Bar[]</$text></paragraph>' );
  799. editor.editing.view.document.fire( 'mousedown' );
  800. editor.editing.view.document.fire( 'selectionChange', {
  801. newSelection: view.document.selection
  802. } );
  803. expect( getModelData( model ) ).to.equal(
  804. '<paragraph><$text bold="true" linkHref="url">Bar</$text><$text bold="true">[]</$text></paragraph>'
  805. );
  806. editor.execute( 'input', { text: 'Foo' } );
  807. expect( getModelData( model ) ).to.equal(
  808. '<paragraph><$text bold="true" linkHref="url">Bar</$text><$text bold="true">Foo[]</$text></paragraph>'
  809. );
  810. } );
  811. it( 'should do nothing if the text was not clicked', () => {
  812. setModelData( model, '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
  813. editor.editing.view.document.fire( 'selectionChange', {
  814. newSelection: view.document.selection
  815. } );
  816. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
  817. } );
  818. it( 'should do nothing if the selection is not collapsed after the click', () => {
  819. setModelData( model, '<paragraph>[<$text linkHref="url">Bar</$text>]</paragraph>' );
  820. editor.editing.view.document.fire( 'mousedown' );
  821. editor.editing.view.document.fire( 'selectionChange', {
  822. newSelection: view.document.selection
  823. } );
  824. expect( getModelData( model ) ).to.equal( '<paragraph>[<$text linkHref="url">Bar</$text>]</paragraph>' );
  825. } );
  826. it( 'should do nothing if the text is not a link', () => {
  827. setModelData( model, '<paragraph><$text bold="true">Bar[]</$text></paragraph>' );
  828. editor.editing.view.document.fire( 'mousedown' );
  829. editor.editing.view.document.fire( 'selectionChange', {
  830. newSelection: view.document.selection
  831. } );
  832. expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bar[]</$text></paragraph>' );
  833. } );
  834. it( 'should remove manual decorators', () => {
  835. setModelData( model, '<paragraph><$text linkIsFoo="true" linkIsBar="true" linkHref="url">Bar[]</$text></paragraph>' );
  836. editor.editing.view.document.fire( 'mousedown' );
  837. editor.editing.view.document.fire( 'selectionChange', {
  838. newSelection: view.document.selection
  839. } );
  840. expect( getModelData( model ) ).to.equal(
  841. '<paragraph><$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar</$text>[]</paragraph>'
  842. );
  843. editor.execute( 'input', { text: 'Foo' } );
  844. expect( getModelData( model ) ).to.equal(
  845. '<paragraph><$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar</$text>Foo[]</paragraph>'
  846. );
  847. } );
  848. } );
  849. // https://github.com/ckeditor/ckeditor5/issues/4762
  850. describe( 'typing over the link', () => {
  851. let editor;
  852. beforeEach( async () => {
  853. editor = await ClassicTestEditor.create( element, {
  854. plugins: [ Paragraph, LinkEditing, Enter, BoldEditing, ItalicEditing, ImageEditing ],
  855. link: {
  856. decorators: {
  857. isFoo: {
  858. mode: 'manual',
  859. label: 'Foo',
  860. attributes: {
  861. class: 'foo'
  862. }
  863. },
  864. isBar: {
  865. mode: 'manual',
  866. label: 'Bar',
  867. attributes: {
  868. target: '_blank'
  869. }
  870. }
  871. }
  872. }
  873. } );
  874. model = editor.model;
  875. view = editor.editing.view;
  876. model.schema.extend( '$text', {
  877. allowIn: '$root',
  878. allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
  879. } );
  880. } );
  881. afterEach( async () => {
  882. await editor.destroy();
  883. } );
  884. it( 'should require Clipboard plugin', () => {
  885. expect( LinkEditing.requires.includes( Clipboard ) ).to.equal( true );
  886. } );
  887. it( 'should require Input plugin', () => {
  888. expect( LinkEditing.requires.includes( Input ) ).to.equal( true );
  889. } );
  890. it( 'should preserve selection attributes when the entire link is selected', () => {
  891. setModelData( model,
  892. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  893. );
  894. editor.execute( 'input', {
  895. text: 'Abcde'
  896. } );
  897. expect( getModelData( model ) ).to.equal(
  898. '<paragraph>This is <$text linkHref="foo">Abcde</$text>[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  899. );
  900. } );
  901. it( 'should preserve selection attributes when the entire link is selected (mixed attributes in the link)', () => {
  902. setModelData( model,
  903. '<paragraph>' +
  904. 'This is [' +
  905. '<$text linkHref="foo" italic="true">F</$text>' +
  906. '<$text linkHref="foo" bold="true">o</$text>' +
  907. '<$text linkHref="foo" bold="true" italic="true">o</$text>' +
  908. '<$text linkHref="foo" bold="true">B</$text>' +
  909. '<$text linkHref="foo" bold="true" italic="true">a</$text>' +
  910. '<$text linkHref="foo">r</$text>]' +
  911. ' from ' +
  912. '<$text linkHref="bar">Bar</$text>' +
  913. '.' +
  914. '</paragraph>'
  915. );
  916. editor.execute( 'input', {
  917. text: 'Abcde'
  918. } );
  919. expect( getModelData( model ) ).to.equal(
  920. '<paragraph>' +
  921. 'This is ' +
  922. '<$text italic="true" linkHref="foo">Abcde</$text>' +
  923. '<$text italic="true">[]</$text>' +
  924. ' from ' +
  925. '<$text linkHref="bar">Bar</$text>' +
  926. '.' +
  927. '</paragraph>'
  928. );
  929. } );
  930. it( 'should preserve selection attributes when the selection starts at the beginning of the link', () => {
  931. setModelData( model,
  932. '<paragraph>This is [<$text linkHref="foo">Fo]o</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
  933. );
  934. editor.execute( 'input', {
  935. text: 'Abcde'
  936. } );
  937. expect( getModelData( model ) ).to.equal(
  938. '<paragraph>This is <$text linkHref="foo">Abcde[]o</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
  939. );
  940. } );
  941. it( 'should preserve selection attributes when it starts at the beginning of the link (mixed attributes in the link)', () => {
  942. setModelData( model,
  943. '<paragraph>' +
  944. 'This is [' +
  945. '<$text linkHref="foo" italic="true">F</$text>' +
  946. '<$text linkHref="foo" bold="true">o</$text>' +
  947. '<$text linkHref="foo" bold="true" italic="true">o</$text>' +
  948. '<$text linkHref="foo" bold="true">B</$text>' +
  949. '<$text linkHref="foo" bold="true" italic="true">a]</$text>' +
  950. '<$text linkHref="foo">r</$text>' +
  951. ' from ' +
  952. '<$text linkHref="bar">Bar</$text>' +
  953. '.' +
  954. '</paragraph>'
  955. );
  956. editor.execute( 'input', {
  957. text: 'Abcde'
  958. } );
  959. expect( getModelData( model ) ).to.equal(
  960. '<paragraph>' +
  961. 'This is ' +
  962. '<$text italic="true" linkHref="foo">Abcde[]</$text>' +
  963. '<$text linkHref="foo">r</$text>' +
  964. ' from ' +
  965. '<$text linkHref="bar">Bar</$text>' +
  966. '.' +
  967. '</paragraph>'
  968. );
  969. } );
  970. it( 'should preserve all attributes of the link node (decorators check)', () => {
  971. setModelData( model,
  972. '<paragraph>' +
  973. 'This is ' +
  974. '<$text linkIsFoo="true" linkIsBar="true" linkHref="foo">[Foo]</$text>' +
  975. ' from ' +
  976. '<$text linkHref="bar">Bar</$text>' +
  977. '.' +
  978. '</paragraph>'
  979. );
  980. editor.execute( 'input', {
  981. text: 'Abcde'
  982. } );
  983. expect( getModelData( model ) ).to.equal(
  984. '<paragraph>' +
  985. 'This is ' +
  986. '<$text linkHref="foo" linkIsBar="true" linkIsFoo="true">Abcde</$text>[]' +
  987. ' from ' +
  988. '<$text linkHref="bar">Bar</$text>' +
  989. '.' +
  990. '</paragraph>'
  991. );
  992. } );
  993. it( 'should not preserve selection attributes when the changes are not caused by typing', () => {
  994. setModelData( model,
  995. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  996. );
  997. model.change( writer => {
  998. model.deleteContent( model.document.selection );
  999. model.insertContent( writer.createText( 'Abcde' ) );
  1000. } );
  1001. expect( getModelData( model ) ).to.equal(
  1002. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1003. );
  1004. } );
  1005. it( 'should not preserve selection attributes when the changes are not caused by typing (pasting check)', () => {
  1006. setModelData( model,
  1007. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1008. );
  1009. view.document.fire( 'paste', {
  1010. dataTransfer: createDataTransfer( {
  1011. 'text/html': '<p>Abcde</p>',
  1012. 'text/plain': 'Abcde'
  1013. } ),
  1014. preventDefault: sinon.spy(),
  1015. stopPropagation: sinon.spy()
  1016. } );
  1017. expect( getModelData( model ) ).to.equal(
  1018. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1019. );
  1020. } );
  1021. it( 'should not preserve selection attributes when typed after cutting the content', () => {
  1022. setModelData( model,
  1023. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1024. );
  1025. view.document.fire( 'cut', {
  1026. dataTransfer: createDataTransfer(),
  1027. preventDefault: sinon.spy(),
  1028. stopPropagation: sinon.spy()
  1029. } );
  1030. editor.execute( 'input', {
  1031. text: 'Abcde'
  1032. } );
  1033. expect( getModelData( model ) ).to.equal(
  1034. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1035. );
  1036. } );
  1037. it( 'should not preserve anything if selected an element instead of text', () => {
  1038. setModelData( model,
  1039. '[<image src="/assets/sample.png"></image>]'
  1040. );
  1041. editor.execute( 'input', {
  1042. text: 'Abcde'
  1043. } );
  1044. expect( getModelData( model ) ).to.equal(
  1045. 'Abcde[]'
  1046. );
  1047. } );
  1048. it( 'should not preserve anything if selected text does not have the `linkHref` attribute`', () => {
  1049. setModelData( model,
  1050. '<paragraph>This is [<$text bold="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1051. );
  1052. editor.execute( 'input', {
  1053. text: 'Abcde'
  1054. } );
  1055. expect( getModelData( model ) ).to.equal(
  1056. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1057. );
  1058. } );
  1059. it( 'should not preserve selection attributes when the entire link is selected and pressed "Backspace"', () => {
  1060. setModelData( model,
  1061. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1062. );
  1063. view.document.fire( 'delete', new DomEventData( view.document, {
  1064. keyCode: keyCodes.backspace,
  1065. preventDefault: () => {}
  1066. } ) );
  1067. editor.execute( 'input', {
  1068. text: 'Abcde'
  1069. } );
  1070. expect( getModelData( model ) ).to.equal(
  1071. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1072. );
  1073. } );
  1074. it( 'should not preserve selection attributes when the entire link is selected and pressed "Delete"', () => {
  1075. setModelData( model,
  1076. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1077. );
  1078. view.document.fire( 'delete', new DomEventData( view.document, {
  1079. keyCode: keyCodes.delete,
  1080. preventDefault: () => {}
  1081. } ) );
  1082. editor.execute( 'input', {
  1083. text: 'Abcde'
  1084. } );
  1085. expect( getModelData( model ) ).to.equal(
  1086. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1087. );
  1088. } );
  1089. it( 'should not preserve selection attributes when selected different links', () => {
  1090. setModelData( model,
  1091. '<paragraph>This is <$text linkHref="foo">[Foo</$text> from <$text linkHref="bar">Bar]</$text>.</paragraph>'
  1092. );
  1093. editor.execute( 'input', {
  1094. text: 'Abcde'
  1095. } );
  1096. expect( getModelData( model ) ).to.equal( '<paragraph>This is Abcde[].</paragraph>' );
  1097. } );
  1098. it( 'should not preserve selection attributes when selected more than single link (start of the selection)', () => {
  1099. setModelData( model,
  1100. '<paragraph>This is[ <$text linkHref="foo">Foo]</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1101. );
  1102. editor.execute( 'input', {
  1103. text: 'Abcde'
  1104. } );
  1105. expect( getModelData( model ) ).to.equal(
  1106. '<paragraph>This isAbcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1107. );
  1108. } );
  1109. it( 'should not preserve selection attributes when selected more than single link (end of the selection)', () => {
  1110. setModelData( model,
  1111. '<paragraph>This is <$text linkHref="foo">[Foo</$text> ]from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1112. );
  1113. editor.execute( 'input', {
  1114. text: 'Abcde'
  1115. } );
  1116. expect( getModelData( model ) ).to.equal(
  1117. '<paragraph>This is Abcde[]from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1118. );
  1119. } );
  1120. function createDataTransfer( data ) {
  1121. return {
  1122. getData( type ) {
  1123. return data[ type ];
  1124. },
  1125. setData() {}
  1126. };
  1127. }
  1128. } );
  1129. } );