linkediting.js 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403
  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. it( 'stores decorators in LinkCommand#automaticDecorators collection', () => {
  583. expect( editor.commands.get( 'link' ).automaticDecorators.length ).to.equal( 3 );
  584. } );
  585. } );
  586. } );
  587. describe( 'custom linkHref converter', () => {
  588. beforeEach( async () => {
  589. class CustomLinks extends Plugin {
  590. init() {
  591. const editor = this.editor;
  592. editor.conversion.for( 'downcast' ).add( dispatcher => {
  593. dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => {
  594. conversionApi.consumable.consume( data.item, 'attribute:linkHref' );
  595. // Very simplified downcast just for test assertion.
  596. const viewWriter = conversionApi.writer;
  597. const linkElement = viewWriter.createAttributeElement(
  598. 'a',
  599. {
  600. href: data.attributeNewValue
  601. }, {
  602. priority: 5
  603. }
  604. );
  605. viewWriter.setCustomProperty( 'link', true, linkElement );
  606. viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), linkElement );
  607. }, { priority: 'highest' } );
  608. } );
  609. }
  610. }
  611. await editor.destroy();
  612. editor = await ClassicTestEditor.create( element, {
  613. plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ],
  614. link: {
  615. addTargetToExternalLinks: true
  616. }
  617. } );
  618. model = editor.model;
  619. view = editor.editing.view;
  620. } );
  621. it( 'has possibility to override default one', () => {
  622. editor.setData( '<p><a href="http://example.com">foo</a>bar</p>' );
  623. expect( getModelData( model, { withoutSelection: true } ) )
  624. .to.equal( '<paragraph><$text linkHref="http://example.com">foo</$text>bar</paragraph>' );
  625. expect( editor.getData() ).to.equal( '<p><a href="http://example.com">foo</a>bar</p>' );
  626. } );
  627. } );
  628. describe( 'upcast converter', () => {
  629. let element, editor;
  630. beforeEach( () => {
  631. element = document.createElement( 'div' );
  632. document.body.appendChild( element );
  633. } );
  634. afterEach( () => {
  635. element.remove();
  636. } );
  637. it( 'should upcast attributes from initial data', async () => {
  638. editor = await ClassicTestEditor.create( element, {
  639. initialData: '<p><a href="url" target="_blank" rel="noopener noreferrer" download="file">Foo</a>' +
  640. '<a href="example.com" download="file">Bar</a></p>',
  641. plugins: [ Paragraph, LinkEditing, Enter ],
  642. link: {
  643. decorators: {
  644. isExternal: {
  645. mode: 'manual',
  646. label: 'Open in a new window',
  647. attributes: {
  648. target: '_blank',
  649. rel: 'noopener noreferrer'
  650. }
  651. },
  652. isDownloadable: {
  653. mode: 'manual',
  654. label: 'Downloadable',
  655. attributes: {
  656. download: 'file'
  657. }
  658. }
  659. }
  660. }
  661. } );
  662. model = editor.model;
  663. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  664. '<paragraph>' +
  665. '<$text linkHref="url" linkIsDownloadable="true" linkIsExternal="true">Foo</$text>' +
  666. '<$text linkHref="example.com" linkIsDownloadable="true">Bar</$text>' +
  667. '</paragraph>'
  668. );
  669. await editor.destroy();
  670. } );
  671. it( 'should not upcast partial and incorrect attributes', async () => {
  672. editor = await ClassicTestEditor.create( element, {
  673. initialData: '<p><a href="url" target="_blank" download="something">Foo</a>' +
  674. '<a href="example.com" download="test">Bar</a></p>',
  675. plugins: [ Paragraph, LinkEditing, Enter ],
  676. link: {
  677. decorators: {
  678. isExternal: {
  679. mode: 'manual',
  680. label: 'Open in a new window',
  681. attributes: {
  682. target: '_blank',
  683. rel: 'noopener noreferrer'
  684. }
  685. },
  686. isDownloadable: {
  687. mode: 'manual',
  688. label: 'Downloadable',
  689. attributes: {
  690. download: 'file'
  691. }
  692. }
  693. }
  694. }
  695. } );
  696. model = editor.model;
  697. expect( getModelData( model, { withoutSelection: true } ) ).to.equal(
  698. '<paragraph>' +
  699. '<$text linkHref="url">Foo</$text>' +
  700. '<$text linkHref="example.com">Bar</$text>' +
  701. '</paragraph>'
  702. );
  703. await editor.destroy();
  704. } );
  705. } );
  706. } );
  707. // https://github.com/ckeditor/ckeditor5/issues/1016
  708. describe( 'typing around the link after a click', () => {
  709. let editor;
  710. beforeEach( async () => {
  711. editor = await ClassicTestEditor.create( element, {
  712. plugins: [ Paragraph, LinkEditing, Enter, Input, BoldEditing ],
  713. link: {
  714. decorators: {
  715. isFoo: {
  716. mode: 'manual',
  717. label: 'Foo',
  718. attributes: {
  719. class: 'foo'
  720. }
  721. },
  722. isBar: {
  723. mode: 'manual',
  724. label: 'Bar',
  725. attributes: {
  726. target: '_blank'
  727. }
  728. }
  729. }
  730. }
  731. } );
  732. model = editor.model;
  733. view = editor.editing.view;
  734. model.schema.extend( '$text', {
  735. allowIn: '$root',
  736. allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
  737. } );
  738. } );
  739. afterEach( async () => {
  740. await editor.destroy();
  741. } );
  742. it( 'should insert content after the link', () => {
  743. setModelData( model, '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
  744. editor.editing.view.document.fire( 'mousedown' );
  745. editor.editing.view.document.fire( 'selectionChange', {
  746. newSelection: view.document.selection
  747. } );
  748. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar</$text>[]</paragraph>' );
  749. editor.execute( 'input', { text: 'Foo' } );
  750. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar</$text>Foo[]</paragraph>' );
  751. } );
  752. it( 'should insert content before the link', () => {
  753. setModelData( model, '<paragraph><$text linkHref="url">[]Bar</$text></paragraph>' );
  754. editor.editing.view.document.fire( 'mousedown' );
  755. editor.editing.view.document.fire( 'selectionChange', {
  756. newSelection: view.document.selection
  757. } );
  758. expect( getModelData( model ) ).to.equal( '<paragraph>[]<$text linkHref="url">Bar</$text></paragraph>' );
  759. editor.execute( 'input', { text: 'Foo' } );
  760. expect( getModelData( model ) ).to.equal( '<paragraph>Foo[]<$text linkHref="url">Bar</$text></paragraph>' );
  761. } );
  762. it( 'should insert content to the link if clicked inside it', () => {
  763. setModelData( model, '<paragraph><$text linkHref="url">B[]ar</$text></paragraph>' );
  764. editor.editing.view.document.fire( 'mousedown' );
  765. editor.editing.view.document.fire( 'selectionChange', {
  766. newSelection: view.document.selection
  767. } );
  768. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">B[]ar</$text></paragraph>' );
  769. editor.execute( 'input', { text: 'ar. B' } );
  770. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar. B[]ar</$text></paragraph>' );
  771. } );
  772. it( 'should insert content between two links (selection at the end of the first link)', () => {
  773. setModelData( model, '<paragraph><$text linkHref="foo">Foo[]</$text><$text linkHref="bar">Bar</$text></paragraph>' );
  774. editor.editing.view.document.fire( 'mousedown' );
  775. editor.editing.view.document.fire( 'selectionChange', {
  776. newSelection: view.document.selection
  777. } );
  778. expect( getModelData( model ) ).to.equal(
  779. '<paragraph><$text linkHref="foo">Foo</$text>[]<$text linkHref="bar">Bar</$text></paragraph>'
  780. );
  781. editor.execute( 'input', { text: 'Foo' } );
  782. expect( getModelData( model ) ).to.equal(
  783. '<paragraph><$text linkHref="foo">Foo</$text>Foo[]<$text linkHref="bar">Bar</$text></paragraph>'
  784. );
  785. } );
  786. it( 'should insert content between two links (selection at the beginning of the second link)', () => {
  787. setModelData( model, '<paragraph><$text linkHref="foo">Foo</$text><$text linkHref="bar">[]Bar</$text></paragraph>' );
  788. editor.editing.view.document.fire( 'mousedown' );
  789. editor.editing.view.document.fire( 'selectionChange', {
  790. newSelection: view.document.selection
  791. } );
  792. expect( getModelData( model ) ).to.equal(
  793. '<paragraph><$text linkHref="foo">Foo</$text>[]<$text linkHref="bar">Bar</$text></paragraph>'
  794. );
  795. editor.execute( 'input', { text: 'Foo' } );
  796. expect( getModelData( model ) ).to.equal(
  797. '<paragraph><$text linkHref="foo">Foo</$text>Foo[]<$text linkHref="bar">Bar</$text></paragraph>'
  798. );
  799. } );
  800. it( 'should not touch other attributes than `linkHref`', () => {
  801. setModelData( model, '<paragraph><$text bold="true" linkHref="url">Bar[]</$text></paragraph>' );
  802. editor.editing.view.document.fire( 'mousedown' );
  803. editor.editing.view.document.fire( 'selectionChange', {
  804. newSelection: view.document.selection
  805. } );
  806. expect( getModelData( model ) ).to.equal(
  807. '<paragraph><$text bold="true" linkHref="url">Bar</$text><$text bold="true">[]</$text></paragraph>'
  808. );
  809. editor.execute( 'input', { text: 'Foo' } );
  810. expect( getModelData( model ) ).to.equal(
  811. '<paragraph><$text bold="true" linkHref="url">Bar</$text><$text bold="true">Foo[]</$text></paragraph>'
  812. );
  813. } );
  814. it( 'should do nothing if the text was not clicked', () => {
  815. setModelData( model, '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
  816. editor.editing.view.document.fire( 'selectionChange', {
  817. newSelection: view.document.selection
  818. } );
  819. expect( getModelData( model ) ).to.equal( '<paragraph><$text linkHref="url">Bar[]</$text></paragraph>' );
  820. } );
  821. it( 'should do nothing if the selection is not collapsed after the click', () => {
  822. setModelData( model, '<paragraph>[<$text linkHref="url">Bar</$text>]</paragraph>' );
  823. editor.editing.view.document.fire( 'mousedown' );
  824. editor.editing.view.document.fire( 'selectionChange', {
  825. newSelection: view.document.selection
  826. } );
  827. expect( getModelData( model ) ).to.equal( '<paragraph>[<$text linkHref="url">Bar</$text>]</paragraph>' );
  828. } );
  829. it( 'should do nothing if the text is not a link', () => {
  830. setModelData( model, '<paragraph><$text bold="true">Bar[]</$text></paragraph>' );
  831. editor.editing.view.document.fire( 'mousedown' );
  832. editor.editing.view.document.fire( 'selectionChange', {
  833. newSelection: view.document.selection
  834. } );
  835. expect( getModelData( model ) ).to.equal( '<paragraph><$text bold="true">Bar[]</$text></paragraph>' );
  836. } );
  837. it( 'should remove manual decorators', () => {
  838. setModelData( model, '<paragraph><$text linkIsFoo="true" linkIsBar="true" linkHref="url">Bar[]</$text></paragraph>' );
  839. editor.editing.view.document.fire( 'mousedown' );
  840. editor.editing.view.document.fire( 'selectionChange', {
  841. newSelection: view.document.selection
  842. } );
  843. expect( getModelData( model ) ).to.equal(
  844. '<paragraph><$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar</$text>[]</paragraph>'
  845. );
  846. editor.execute( 'input', { text: 'Foo' } );
  847. expect( getModelData( model ) ).to.equal(
  848. '<paragraph><$text linkHref="url" linkIsBar="true" linkIsFoo="true">Bar</$text>Foo[]</paragraph>'
  849. );
  850. } );
  851. } );
  852. // https://github.com/ckeditor/ckeditor5/issues/4762
  853. describe( 'typing over the link', () => {
  854. let editor;
  855. beforeEach( async () => {
  856. editor = await ClassicTestEditor.create( element, {
  857. plugins: [ Paragraph, LinkEditing, Enter, BoldEditing, ItalicEditing, ImageEditing ],
  858. link: {
  859. decorators: {
  860. isFoo: {
  861. mode: 'manual',
  862. label: 'Foo',
  863. attributes: {
  864. class: 'foo'
  865. }
  866. },
  867. isBar: {
  868. mode: 'manual',
  869. label: 'Bar',
  870. attributes: {
  871. target: '_blank'
  872. }
  873. }
  874. }
  875. }
  876. } );
  877. model = editor.model;
  878. view = editor.editing.view;
  879. model.schema.extend( '$text', {
  880. allowIn: '$root',
  881. allowAttributes: [ 'linkIsFoo', 'linkIsBar' ]
  882. } );
  883. } );
  884. afterEach( async () => {
  885. await editor.destroy();
  886. } );
  887. it( 'should require Clipboard plugin', () => {
  888. expect( LinkEditing.requires.includes( Clipboard ) ).to.equal( true );
  889. } );
  890. it( 'should require Input plugin', () => {
  891. expect( LinkEditing.requires.includes( Input ) ).to.equal( true );
  892. } );
  893. it( 'should preserve selection attributes when the entire link is selected', () => {
  894. setModelData( model,
  895. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  896. );
  897. editor.execute( 'input', {
  898. text: 'Abcde'
  899. } );
  900. expect( getModelData( model ) ).to.equal(
  901. '<paragraph>This is <$text linkHref="foo">Abcde</$text>[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  902. );
  903. } );
  904. it( 'should preserve selection attributes when the entire link is selected (mixed attributes in the link)', () => {
  905. setModelData( model,
  906. '<paragraph>' +
  907. 'This is [' +
  908. '<$text linkHref="foo" italic="true">F</$text>' +
  909. '<$text linkHref="foo" bold="true">o</$text>' +
  910. '<$text linkHref="foo" bold="true" italic="true">o</$text>' +
  911. '<$text linkHref="foo" bold="true">B</$text>' +
  912. '<$text linkHref="foo" bold="true" italic="true">a</$text>' +
  913. '<$text linkHref="foo">r</$text>]' +
  914. ' from ' +
  915. '<$text linkHref="bar">Bar</$text>' +
  916. '.' +
  917. '</paragraph>'
  918. );
  919. editor.execute( 'input', {
  920. text: 'Abcde'
  921. } );
  922. expect( getModelData( model ) ).to.equal(
  923. '<paragraph>' +
  924. 'This is ' +
  925. '<$text italic="true" linkHref="foo">Abcde</$text>' +
  926. '<$text italic="true">[]</$text>' +
  927. ' from ' +
  928. '<$text linkHref="bar">Bar</$text>' +
  929. '.' +
  930. '</paragraph>'
  931. );
  932. } );
  933. it( 'should preserve selection attributes when the selection starts at the beginning of the link', () => {
  934. setModelData( model,
  935. '<paragraph>This is [<$text linkHref="foo">Fo]o</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
  936. );
  937. editor.execute( 'input', {
  938. text: 'Abcde'
  939. } );
  940. expect( getModelData( model ) ).to.equal(
  941. '<paragraph>This is <$text linkHref="foo">Abcde[]o</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
  942. );
  943. } );
  944. it( 'should preserve selection attributes when it starts at the beginning of the link (mixed attributes in the link)', () => {
  945. setModelData( model,
  946. '<paragraph>' +
  947. 'This is [' +
  948. '<$text linkHref="foo" italic="true">F</$text>' +
  949. '<$text linkHref="foo" bold="true">o</$text>' +
  950. '<$text linkHref="foo" bold="true" italic="true">o</$text>' +
  951. '<$text linkHref="foo" bold="true">B</$text>' +
  952. '<$text linkHref="foo" bold="true" italic="true">a]</$text>' +
  953. '<$text linkHref="foo">r</$text>' +
  954. ' from ' +
  955. '<$text linkHref="bar">Bar</$text>' +
  956. '.' +
  957. '</paragraph>'
  958. );
  959. editor.execute( 'input', {
  960. text: 'Abcde'
  961. } );
  962. expect( getModelData( model ) ).to.equal(
  963. '<paragraph>' +
  964. 'This is ' +
  965. '<$text italic="true" linkHref="foo">Abcde[]</$text>' +
  966. '<$text linkHref="foo">r</$text>' +
  967. ' from ' +
  968. '<$text linkHref="bar">Bar</$text>' +
  969. '.' +
  970. '</paragraph>'
  971. );
  972. } );
  973. it( 'should preserve all attributes of the link node (decorators check)', () => {
  974. setModelData( model,
  975. '<paragraph>' +
  976. 'This is ' +
  977. '<$text linkIsFoo="true" linkIsBar="true" linkHref="foo">[Foo]</$text>' +
  978. ' from ' +
  979. '<$text linkHref="bar">Bar</$text>' +
  980. '.' +
  981. '</paragraph>'
  982. );
  983. editor.execute( 'input', {
  984. text: 'Abcde'
  985. } );
  986. expect( getModelData( model ) ).to.equal(
  987. '<paragraph>' +
  988. 'This is ' +
  989. '<$text linkHref="foo" linkIsBar="true" linkIsFoo="true">Abcde</$text>[]' +
  990. ' from ' +
  991. '<$text linkHref="bar">Bar</$text>' +
  992. '.' +
  993. '</paragraph>'
  994. );
  995. } );
  996. it( 'should not preserve selection attributes when the changes are not caused by typing', () => {
  997. setModelData( model,
  998. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  999. );
  1000. model.change( writer => {
  1001. model.deleteContent( model.document.selection );
  1002. model.insertContent( writer.createText( 'Abcde' ) );
  1003. } );
  1004. expect( getModelData( model ) ).to.equal(
  1005. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1006. );
  1007. } );
  1008. it( 'should not preserve selection attributes when the changes are not caused by typing (pasting check)', () => {
  1009. setModelData( model,
  1010. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1011. );
  1012. view.document.fire( 'paste', {
  1013. dataTransfer: createDataTransfer( {
  1014. 'text/html': '<p>Abcde</p>',
  1015. 'text/plain': 'Abcde'
  1016. } ),
  1017. preventDefault: sinon.spy(),
  1018. stopPropagation: sinon.spy()
  1019. } );
  1020. expect( getModelData( model ) ).to.equal(
  1021. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1022. );
  1023. } );
  1024. it( 'should not preserve selection attributes when typed after cutting the content', () => {
  1025. setModelData( model,
  1026. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1027. );
  1028. view.document.fire( 'cut', {
  1029. dataTransfer: createDataTransfer(),
  1030. preventDefault: sinon.spy(),
  1031. stopPropagation: sinon.spy()
  1032. } );
  1033. editor.execute( 'input', {
  1034. text: 'Abcde'
  1035. } );
  1036. expect( getModelData( model ) ).to.equal(
  1037. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1038. );
  1039. } );
  1040. it( 'should not preserve anything if selected an element instead of text', () => {
  1041. setModelData( model,
  1042. '[<image src="/assets/sample.png"></image>]'
  1043. );
  1044. editor.execute( 'input', {
  1045. text: 'Abcde'
  1046. } );
  1047. expect( getModelData( model ) ).to.equal(
  1048. 'Abcde[]'
  1049. );
  1050. } );
  1051. it( 'should not preserve anything if selected text does not have the `linkHref` attribute`', () => {
  1052. setModelData( model,
  1053. '<paragraph>This is [<$text bold="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1054. );
  1055. editor.execute( 'input', {
  1056. text: 'Abcde'
  1057. } );
  1058. expect( getModelData( model ) ).to.equal(
  1059. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1060. );
  1061. } );
  1062. it( 'should not preserve selection attributes when the entire link is selected and pressed "Backspace"', () => {
  1063. setModelData( model,
  1064. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1065. );
  1066. view.document.fire( 'delete', new DomEventData( view.document, {
  1067. keyCode: keyCodes.backspace,
  1068. preventDefault: () => {}
  1069. } ) );
  1070. editor.execute( 'input', {
  1071. text: 'Abcde'
  1072. } );
  1073. expect( getModelData( model ) ).to.equal(
  1074. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1075. );
  1076. } );
  1077. it( 'should not preserve selection attributes when the entire link is selected and pressed "Delete"', () => {
  1078. setModelData( model,
  1079. '<paragraph>This is [<$text linkHref="foo">Foo</$text>] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1080. );
  1081. view.document.fire( 'delete', new DomEventData( view.document, {
  1082. keyCode: keyCodes.delete,
  1083. preventDefault: () => {}
  1084. } ) );
  1085. editor.execute( 'input', {
  1086. text: 'Abcde'
  1087. } );
  1088. expect( getModelData( model ) ).to.equal(
  1089. '<paragraph>This is Abcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1090. );
  1091. } );
  1092. it( 'should not preserve selection attributes when selected different links', () => {
  1093. setModelData( model,
  1094. '<paragraph>This is <$text linkHref="foo">[Foo</$text> from <$text linkHref="bar">Bar]</$text>.</paragraph>'
  1095. );
  1096. editor.execute( 'input', {
  1097. text: 'Abcde'
  1098. } );
  1099. expect( getModelData( model ) ).to.equal( '<paragraph>This is Abcde[].</paragraph>' );
  1100. } );
  1101. it( 'should not preserve selection attributes when selected more than single link (start of the selection)', () => {
  1102. setModelData( model,
  1103. '<paragraph>This is[ <$text linkHref="foo">Foo]</$text> from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1104. );
  1105. editor.execute( 'input', {
  1106. text: 'Abcde'
  1107. } );
  1108. expect( getModelData( model ) ).to.equal(
  1109. '<paragraph>This isAbcde[] from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1110. );
  1111. } );
  1112. it( 'should not preserve selection attributes when selected more than single link (end of the selection)', () => {
  1113. setModelData( model,
  1114. '<paragraph>This is <$text linkHref="foo">[Foo</$text> ]from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1115. );
  1116. editor.execute( 'input', {
  1117. text: 'Abcde'
  1118. } );
  1119. expect( getModelData( model ) ).to.equal(
  1120. '<paragraph>This is Abcde[]from <$text linkHref="bar">Bar</$text>.</paragraph>'
  1121. );
  1122. } );
  1123. function createDataTransfer( data ) {
  1124. return {
  1125. getData( type ) {
  1126. return data[ type ];
  1127. },
  1128. setData() {}
  1129. };
  1130. }
  1131. } );
  1132. } );