linkui.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals document, Event */
  6. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  7. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  8. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  9. import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  10. import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
  11. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  12. import LinkEditing from '../src/linkediting';
  13. import LinkUI from '../src/linkui';
  14. import LinkFormView from '../src/ui/linkformview';
  15. import LinkActionsView from '../src/ui/linkactionsview';
  16. import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
  17. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  18. import Range from '@ckeditor/ckeditor5-engine/src/view/range';
  19. import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
  20. describe( 'LinkUI', () => {
  21. let editor, linkUIFeature, linkButton, balloon, formView, actionsView, editorElement;
  22. testUtils.createSinonSandbox();
  23. beforeEach( () => {
  24. editorElement = document.createElement( 'div' );
  25. document.body.appendChild( editorElement );
  26. return ClassicTestEditor
  27. .create( editorElement, {
  28. plugins: [ LinkEditing, LinkUI, Paragraph ]
  29. } )
  30. .then( newEditor => {
  31. editor = newEditor;
  32. linkUIFeature = editor.plugins.get( LinkUI );
  33. linkButton = editor.ui.componentFactory.create( 'link' );
  34. balloon = editor.plugins.get( ContextualBalloon );
  35. formView = linkUIFeature.formView;
  36. actionsView = linkUIFeature.actionsView;
  37. // There is no point to execute BalloonPanelView attachTo and pin methods so lets override it.
  38. testUtils.sinon.stub( balloon.view, 'attachTo' ).returns( {} );
  39. testUtils.sinon.stub( balloon.view, 'pin' ).returns( {} );
  40. formView.render();
  41. } );
  42. } );
  43. afterEach( () => {
  44. editorElement.remove();
  45. return editor.destroy();
  46. } );
  47. it( 'should load ContextualBalloon', () => {
  48. expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
  49. } );
  50. describe( 'init', () => {
  51. it( 'should register click observer', () => {
  52. expect( editor.editing.view.getObserver( ClickObserver ) ).to.be.instanceOf( ClickObserver );
  53. } );
  54. it( 'should create #actionsView', () => {
  55. expect( actionsView ).to.be.instanceOf( LinkActionsView );
  56. } );
  57. it( 'should create #formView', () => {
  58. expect( formView ).to.be.instanceOf( LinkFormView );
  59. } );
  60. describe( 'link toolbar button', () => {
  61. it( 'should be registered', () => {
  62. expect( linkButton ).to.be.instanceOf( ButtonView );
  63. } );
  64. it( 'should be bound to the link command', () => {
  65. const command = editor.commands.get( 'link' );
  66. command.isEnabled = true;
  67. command.value = true;
  68. expect( linkButton.isOn ).to.be.true;
  69. expect( linkButton.isEnabled ).to.be.true;
  70. command.isEnabled = false;
  71. command.value = false;
  72. expect( linkButton.isOn ).to.be.false;
  73. expect( linkButton.isEnabled ).to.be.false;
  74. } );
  75. it( 'should call #_showUI upon #execute', () => {
  76. const spy = testUtils.sinon.stub( linkUIFeature, '_showUI' ).returns( {} );
  77. linkButton.fire( 'execute' );
  78. sinon.assert.calledWithExactly( spy );
  79. } );
  80. } );
  81. } );
  82. describe( '_showUI()', () => {
  83. let balloonAddSpy;
  84. beforeEach( () => {
  85. balloonAddSpy = testUtils.sinon.spy( balloon, 'add' );
  86. editor.editing.view.document.isFocused = true;
  87. } );
  88. it( 'should not work if the link command is disabled', () => {
  89. setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
  90. editor.commands.get( 'link' ).isEnabled = false;
  91. linkUIFeature._showUI();
  92. expect( balloon.visibleView ).to.be.null;
  93. } );
  94. it( 'should not throw if the UI is already visible', () => {
  95. setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
  96. linkUIFeature._showUI();
  97. expect( () => {
  98. linkUIFeature._showUI();
  99. } ).to.not.throw();
  100. } );
  101. it( 'should add #formView to the balloon and attach the balloon to the selection when text fragment is selected', () => {
  102. setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
  103. const selectedRange = editorElement.ownerDocument.getSelection().getRangeAt( 0 );
  104. linkUIFeature._showUI();
  105. expect( balloon.visibleView ).to.equal( formView );
  106. sinon.assert.calledWithExactly( balloonAddSpy, {
  107. view: formView,
  108. position: {
  109. target: selectedRange
  110. }
  111. } );
  112. } );
  113. it( 'should add #formView to the balloon and attach the balloon to the selection when selection is collapsed', () => {
  114. setModelData( editor.model, '<paragraph>f[]oo</paragraph>' );
  115. const selectedRange = editorElement.ownerDocument.getSelection().getRangeAt( 0 );
  116. linkUIFeature._showUI();
  117. expect( balloon.visibleView ).to.equal( formView );
  118. sinon.assert.calledWithExactly( balloonAddSpy, {
  119. view: formView,
  120. position: {
  121. target: selectedRange
  122. }
  123. } );
  124. } );
  125. it( 'should add #actionsView to the balloon and attach the balloon to the link element when collapsed selection is inside ' +
  126. 'that link',
  127. () => {
  128. setModelData( editor.model, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' );
  129. const linkElement = editor.editing.view.getDomRoot().querySelector( 'a' );
  130. linkUIFeature._showUI();
  131. expect( balloon.visibleView ).to.equal( actionsView );
  132. sinon.assert.calledWithExactly( balloonAddSpy, {
  133. view: actionsView,
  134. position: {
  135. target: linkElement
  136. }
  137. } );
  138. } );
  139. // #https://github.com/ckeditor/ckeditor5-link/issues/181
  140. it( 'should add #formView to the balloon when collapsed selection is inside the link and #actionsView is already visible', () => {
  141. setModelData( editor.model, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' );
  142. const linkElement = editor.editing.view.getDomRoot().querySelector( 'a' );
  143. linkUIFeature._showUI();
  144. expect( balloon.visibleView ).to.equal( actionsView );
  145. sinon.assert.calledWithExactly( balloonAddSpy, {
  146. view: actionsView,
  147. position: {
  148. target: linkElement
  149. }
  150. } );
  151. linkUIFeature._showUI();
  152. expect( balloon.visibleView ).to.equal( formView );
  153. sinon.assert.calledWithExactly( balloonAddSpy, {
  154. view: formView,
  155. position: {
  156. target: linkElement
  157. }
  158. } );
  159. } );
  160. it( 'should disable #formView and #actionsView elements when link and unlink commands are disabled', () => {
  161. setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
  162. linkUIFeature._showUI();
  163. editor.commands.get( 'link' ).isEnabled = true;
  164. editor.commands.get( 'unlink' ).isEnabled = true;
  165. expect( formView.urlInputView.isReadOnly ).to.be.false;
  166. expect( formView.saveButtonView.isEnabled ).to.be.true;
  167. expect( formView.cancelButtonView.isEnabled ).to.be.true;
  168. expect( actionsView.unlinkButtonView.isEnabled ).to.be.true;
  169. expect( actionsView.editButtonView.isEnabled ).to.be.true;
  170. editor.commands.get( 'link' ).isEnabled = false;
  171. editor.commands.get( 'unlink' ).isEnabled = false;
  172. expect( formView.urlInputView.isReadOnly ).to.be.true;
  173. expect( formView.saveButtonView.isEnabled ).to.be.false;
  174. expect( formView.cancelButtonView.isEnabled ).to.be.true;
  175. expect( actionsView.unlinkButtonView.isEnabled ).to.be.false;
  176. expect( actionsView.editButtonView.isEnabled ).to.be.false;
  177. } );
  178. // https://github.com/ckeditor/ckeditor5-link/issues/78
  179. it( 'should make sure the URL input in the #formView always stays in sync with the value of the command (selected link)', () => {
  180. setModelData( editor.model, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' );
  181. // Mock some leftover value **in DOM**, e.g. after previous editing.
  182. formView.urlInputView.inputView.element.value = 'leftover';
  183. linkUIFeature._showUI();
  184. actionsView.fire( 'edit' );
  185. expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' );
  186. } );
  187. // https://github.com/ckeditor/ckeditor5-link/issues/123
  188. it( 'should make sure the URL input in the #formView always stays in sync with the value of the command (no link selected)', () => {
  189. setModelData( editor.model, '<paragraph>f[]oo</paragraph>' );
  190. linkUIFeature._showUI();
  191. expect( formView.urlInputView.inputView.element.value ).to.equal( '' );
  192. } );
  193. describe( 'response to ui#update', () => {
  194. let view, viewDocument;
  195. beforeEach( () => {
  196. view = editor.editing.view;
  197. viewDocument = view.document;
  198. } );
  199. it( 'should not duplicate #update listeners', () => {
  200. setModelData( editor.model, '<paragraph>f[]oo</paragraph>' );
  201. const spy = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
  202. linkUIFeature._showUI();
  203. editor.ui.fire( 'update' );
  204. linkUIFeature._hideUI();
  205. linkUIFeature._showUI();
  206. editor.ui.fire( 'update' );
  207. sinon.assert.calledTwice( spy );
  208. } );
  209. // https://github.com/ckeditor/ckeditor5-link/issues/113
  210. it( 'updates the position of the panel – editing a link, then the selection remains in the link', () => {
  211. setModelData( editor.model, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' );
  212. linkUIFeature._showUI();
  213. const spy = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
  214. expect( getViewData( view ) ).to.equal(
  215. '<p><a class="ck-link_selected" href="url">f{}oo</a></p>'
  216. );
  217. const root = viewDocument.getRoot();
  218. const linkElement = root.getChild( 0 ).getChild( 0 );
  219. const text = linkElement.getChild( 0 );
  220. // Move selection to foo[].
  221. view.change( writer => {
  222. writer.setSelection( Range.createFromParentsAndOffsets( text, 3, text, 3 ), true );
  223. } );
  224. sinon.assert.calledOnce( spy );
  225. sinon.assert.calledWithExactly( spy, {
  226. target: view.domConverter.mapViewToDom( linkElement )
  227. } );
  228. } );
  229. // https://github.com/ckeditor/ckeditor5-link/issues/113
  230. it( 'updates the position of the panel – creating a new link, then the selection moved', () => {
  231. setModelData( editor.model, '<paragraph>f[]oo</paragraph>' );
  232. linkUIFeature._showUI();
  233. const spy = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
  234. const root = viewDocument.getRoot();
  235. const text = root.getChild( 0 ).getChild( 0 );
  236. view.change( writer => {
  237. writer.setSelection( Range.createFromParentsAndOffsets( text, 3, text, 3 ), true );
  238. } );
  239. sinon.assert.calledOnce( spy );
  240. sinon.assert.calledWithExactly( spy, {
  241. target: editorElement.ownerDocument.getSelection().getRangeAt( 0 )
  242. } );
  243. } );
  244. // https://github.com/ckeditor/ckeditor5-link/issues/113
  245. it( 'hides of the panel – editing a link, then the selection moved out of the link', () => {
  246. setModelData( editor.model, '<paragraph><$text linkHref="url">f[]oo</$text>bar</paragraph>' );
  247. linkUIFeature._showUI();
  248. const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
  249. const spyHide = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  250. const root = viewDocument.getRoot();
  251. const text = root.getChild( 0 ).getChild( 1 );
  252. // Move selection to b[]ar.
  253. view.change( writer => {
  254. writer.setSelection( Range.createFromParentsAndOffsets( text, 1, text, 1 ), true );
  255. } );
  256. sinon.assert.calledOnce( spyHide );
  257. sinon.assert.notCalled( spyUpdate );
  258. } );
  259. // https://github.com/ckeditor/ckeditor5-link/issues/113
  260. it( 'hides the panel – editing a link, then the selection expands', () => {
  261. setModelData( editor.model, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' );
  262. linkUIFeature._showUI();
  263. const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
  264. const spyHide = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  265. expect( getViewData( view ) ).to.equal( '<p><a class="ck-link_selected" href="url">f{}oo</a></p>' );
  266. const root = viewDocument.getRoot();
  267. const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 );
  268. // Move selection to f[o]o.
  269. view.change( writer => {
  270. writer.setSelection( Range.createFromParentsAndOffsets( text, 1, text, 2 ), true );
  271. } );
  272. sinon.assert.calledOnce( spyHide );
  273. sinon.assert.notCalled( spyUpdate );
  274. } );
  275. // https://github.com/ckeditor/ckeditor5-link/issues/113
  276. it( 'hides the panel – creating a new link, then the selection moved to another parent', () => {
  277. setModelData( editor.model, '<paragraph>f[]oo</paragraph><paragraph>bar</paragraph>' );
  278. linkUIFeature._showUI();
  279. const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition' ).returns( {} );
  280. const spyHide = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  281. const root = viewDocument.getRoot();
  282. const text = root.getChild( 1 ).getChild( 0 );
  283. // Move selection to f[o]o.
  284. view.change( writer => {
  285. writer.setSelection( Range.createFromParentsAndOffsets( text, 1, text, 2 ), true );
  286. } );
  287. sinon.assert.calledOnce( spyHide );
  288. sinon.assert.notCalled( spyUpdate );
  289. } );
  290. } );
  291. } );
  292. describe( '_hideUI()', () => {
  293. beforeEach( () => {
  294. linkUIFeature._showUI();
  295. } );
  296. it( 'should remove the UI from the balloon', () => {
  297. expect( balloon.hasView( formView ) ).to.be.true;
  298. expect( balloon.hasView( actionsView ) ).to.be.true;
  299. linkUIFeature._hideUI();
  300. expect( balloon.hasView( formView ) ).to.be.false;
  301. expect( balloon.hasView( actionsView ) ).to.be.false;
  302. } );
  303. it( 'should focus the `editable` by default', () => {
  304. const spy = testUtils.sinon.spy( editor.editing.view, 'focus' );
  305. linkUIFeature._hideUI();
  306. // First call is from _removeFormView.
  307. sinon.assert.calledTwice( spy );
  308. } );
  309. it( 'should not throw an error when views are not in the `balloon`', () => {
  310. linkUIFeature._hideUI();
  311. expect( () => {
  312. linkUIFeature._hideUI();
  313. } ).to.not.throw();
  314. } );
  315. it( 'should clear ui#update listener from the ViewDocument', () => {
  316. const spy = sinon.spy();
  317. linkUIFeature.listenTo( editor.ui, 'update', spy );
  318. linkUIFeature._hideUI();
  319. editor.ui.fire( 'update' );
  320. sinon.assert.notCalled( spy );
  321. } );
  322. } );
  323. describe( 'keyboard support', () => {
  324. it( 'should show the UI on Ctrl+K keystroke', () => {
  325. const spy = testUtils.sinon.stub( linkUIFeature, '_showUI' ).returns( {} );
  326. const command = editor.commands.get( 'link' );
  327. command.isEnabled = false;
  328. editor.keystrokes.press( {
  329. keyCode: keyCodes.k,
  330. ctrlKey: true,
  331. preventDefault: sinon.spy(),
  332. stopPropagation: sinon.spy()
  333. } );
  334. sinon.assert.notCalled( spy );
  335. command.isEnabled = true;
  336. editor.keystrokes.press( {
  337. keyCode: keyCodes.k,
  338. ctrlKey: true,
  339. preventDefault: sinon.spy(),
  340. stopPropagation: sinon.spy()
  341. } );
  342. sinon.assert.calledWithExactly( spy );
  343. } );
  344. it( 'should prevent default action on Ctrl+K keystroke', () => {
  345. const preventDefaultSpy = sinon.spy();
  346. const stopPropagationSpy = sinon.spy();
  347. editor.keystrokes.press( {
  348. keyCode: keyCodes.k,
  349. ctrlKey: true,
  350. preventDefault: preventDefaultSpy,
  351. stopPropagation: stopPropagationSpy
  352. } );
  353. sinon.assert.calledOnce( preventDefaultSpy );
  354. sinon.assert.calledOnce( stopPropagationSpy );
  355. } );
  356. it( 'should focus the the #actionsView on `Tab` key press when #actionsView is visible', () => {
  357. const keyEvtData = {
  358. keyCode: keyCodes.tab,
  359. preventDefault: sinon.spy(),
  360. stopPropagation: sinon.spy()
  361. };
  362. const normalPriorityTabCallbackSpy = sinon.spy();
  363. const highestPriorityTabCallbackSpy = sinon.spy();
  364. editor.keystrokes.set( 'Tab', normalPriorityTabCallbackSpy );
  365. editor.keystrokes.set( 'Tab', highestPriorityTabCallbackSpy, { priority: 'highest' } );
  366. // Balloon is invisible, form not focused.
  367. actionsView.focusTracker.isFocused = false;
  368. const spy = sinon.spy( actionsView, 'focus' );
  369. editor.keystrokes.press( keyEvtData );
  370. sinon.assert.notCalled( keyEvtData.preventDefault );
  371. sinon.assert.notCalled( keyEvtData.stopPropagation );
  372. sinon.assert.notCalled( spy );
  373. sinon.assert.calledOnce( normalPriorityTabCallbackSpy );
  374. sinon.assert.calledOnce( highestPriorityTabCallbackSpy );
  375. // Balloon is visible, form focused.
  376. linkUIFeature._showUI();
  377. testUtils.sinon.stub( linkUIFeature, '_areActionsVisible' ).value( true );
  378. actionsView.focusTracker.isFocused = true;
  379. editor.keystrokes.press( keyEvtData );
  380. sinon.assert.notCalled( keyEvtData.preventDefault );
  381. sinon.assert.notCalled( keyEvtData.stopPropagation );
  382. sinon.assert.notCalled( spy );
  383. sinon.assert.calledTwice( normalPriorityTabCallbackSpy );
  384. sinon.assert.calledTwice( highestPriorityTabCallbackSpy );
  385. // Balloon is still visible, form not focused.
  386. actionsView.focusTracker.isFocused = false;
  387. editor.keystrokes.press( keyEvtData );
  388. sinon.assert.calledOnce( keyEvtData.preventDefault );
  389. sinon.assert.calledOnce( keyEvtData.stopPropagation );
  390. sinon.assert.calledOnce( spy );
  391. sinon.assert.calledTwice( normalPriorityTabCallbackSpy );
  392. sinon.assert.calledThrice( highestPriorityTabCallbackSpy );
  393. } );
  394. it( 'should hide the UI after Esc key press (from editor) and not focus the editable', () => {
  395. const spy = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  396. const keyEvtData = {
  397. keyCode: keyCodes.esc,
  398. preventDefault: sinon.spy(),
  399. stopPropagation: sinon.spy()
  400. };
  401. // Balloon is visible.
  402. linkUIFeature._showUI();
  403. editor.keystrokes.press( keyEvtData );
  404. sinon.assert.calledWithExactly( spy );
  405. } );
  406. it( 'should not hide the UI after Esc key press (from editor) when UI is open but is not visible', () => {
  407. const spy = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  408. const keyEvtData = {
  409. keyCode: keyCodes.esc,
  410. preventDefault: () => {},
  411. stopPropagation: () => {}
  412. };
  413. const viewMock = {
  414. ready: true,
  415. render: () => {},
  416. destroy: () => {}
  417. };
  418. linkUIFeature._showUI();
  419. // Some view precedes the link UI in the balloon.
  420. balloon.add( { view: viewMock } );
  421. editor.keystrokes.press( keyEvtData );
  422. sinon.assert.notCalled( spy );
  423. } );
  424. } );
  425. describe( 'mouse support', () => {
  426. it( 'should hide the UI and not focus editable upon clicking outside the UI', () => {
  427. const spy = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  428. linkUIFeature._showUI( true );
  429. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  430. sinon.assert.calledWithExactly( spy );
  431. } );
  432. it( 'should not hide the UI upon clicking inside the the UI', () => {
  433. const spy = testUtils.sinon.spy( linkUIFeature, '_hideUI' );
  434. linkUIFeature._showUI( true );
  435. balloon.view.element.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  436. sinon.assert.notCalled( spy );
  437. } );
  438. describe( 'clicking on editable', () => {
  439. let observer, spy;
  440. beforeEach( () => {
  441. observer = editor.editing.view.getObserver( ClickObserver );
  442. editor.model.schema.extend( '$text', { allowIn: '$root' } );
  443. spy = testUtils.sinon.stub( linkUIFeature, '_showUI' ).returns( {} );
  444. } );
  445. it( 'should show the UI when collapsed selection is inside link element', () => {
  446. setModelData( editor.model, '<$text linkHref="url">fo[]o</$text>' );
  447. observer.fire( 'click', { target: document.body } );
  448. sinon.assert.calledWithExactly( spy );
  449. } );
  450. it( 'should show the UI when selection exclusively encloses a link element (#1)', () => {
  451. setModelData( editor.model, '[<$text linkHref="url">foo</$text>]' );
  452. observer.fire( 'click', { target: {} } );
  453. sinon.assert.calledWithExactly( spy );
  454. } );
  455. it( 'should show the UI when selection exclusively encloses a link element (#2)', () => {
  456. setModelData( editor.model, '<$text linkHref="url">[foo]</$text>' );
  457. observer.fire( 'click', { target: {} } );
  458. sinon.assert.calledWithExactly( spy );
  459. } );
  460. it( 'should do nothing when selection is not inside link element', () => {
  461. setModelData( editor.model, '[]' );
  462. observer.fire( 'click', { target: {} } );
  463. sinon.assert.notCalled( spy );
  464. } );
  465. it( 'should do nothing when selection is non-collapsed and doesn\'t enclose a link element (#1)', () => {
  466. setModelData( editor.model, '<$text linkHref="url">f[o]o</$text>' );
  467. observer.fire( 'click', { target: {} } );
  468. sinon.assert.notCalled( spy );
  469. } );
  470. it( 'should do nothing when selection is non-collapsed and doesn\'t enclose a link element (#2)', () => {
  471. setModelData( editor.model, '<$text linkHref="url">[fo]o</$text>' );
  472. observer.fire( 'click', { target: {} } );
  473. sinon.assert.notCalled( spy );
  474. } );
  475. it( 'should do nothing when selection is non-collapsed and doesn\'t enclose a link element (#3)', () => {
  476. setModelData( editor.model, '<$text linkHref="url">f[oo]</$text>' );
  477. observer.fire( 'click', { target: {} } );
  478. sinon.assert.notCalled( spy );
  479. } );
  480. it( 'should do nothing when selection is non-collapsed and doesn\'t enclose a link element (#4)', () => {
  481. setModelData( editor.model, 'ba[r<$text linkHref="url">foo]</$text>' );
  482. observer.fire( 'click', { target: {} } );
  483. sinon.assert.notCalled( spy );
  484. } );
  485. it( 'should do nothing when selection is non-collapsed and doesn\'t enclose a link element (#5)', () => {
  486. setModelData( editor.model, 'ba[r<$text linkHref="url">foo</$text>]' );
  487. observer.fire( 'click', { target: {} } );
  488. sinon.assert.notCalled( spy );
  489. } );
  490. } );
  491. } );
  492. describe( 'actions view', () => {
  493. let focusEditableSpy;
  494. beforeEach( () => {
  495. focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
  496. } );
  497. it( 'should mark the editor UI as focused when the #actionsView is focused', () => {
  498. linkUIFeature._showUI();
  499. linkUIFeature._removeFormView();
  500. expect( balloon.visibleView ).to.equal( actionsView );
  501. editor.ui.focusTracker.isFocused = false;
  502. actionsView.element.dispatchEvent( new Event( 'focus' ) );
  503. expect( editor.ui.focusTracker.isFocused ).to.be.true;
  504. } );
  505. describe( 'binding', () => {
  506. it( 'should show the #formView on #edit event and select the URL input field', () => {
  507. linkUIFeature._showUI();
  508. linkUIFeature._removeFormView();
  509. const selectSpy = testUtils.sinon.spy( formView.urlInputView, 'select' );
  510. actionsView.fire( 'edit' );
  511. expect( balloon.visibleView ).to.equal( formView );
  512. sinon.assert.calledOnce( selectSpy );
  513. } );
  514. it( 'should execute unlink command on actionsView#unlink event', () => {
  515. const executeSpy = testUtils.sinon.spy( editor, 'execute' );
  516. actionsView.fire( 'unlink' );
  517. expect( executeSpy.calledOnce ).to.be.true;
  518. expect( executeSpy.calledWithExactly( 'unlink' ) ).to.be.true;
  519. } );
  520. it( 'should hide and focus editable on actionsView#unlink event', () => {
  521. linkUIFeature._showUI();
  522. linkUIFeature._removeFormView();
  523. // Removing the form would call the focus spy.
  524. focusEditableSpy.resetHistory();
  525. actionsView.fire( 'unlink' );
  526. expect( balloon.visibleView ).to.be.null;
  527. expect( focusEditableSpy.calledOnce ).to.be.true;
  528. } );
  529. it( 'should hide after Esc key press', () => {
  530. const keyEvtData = {
  531. keyCode: keyCodes.esc,
  532. preventDefault: sinon.spy(),
  533. stopPropagation: sinon.spy()
  534. };
  535. linkUIFeature._showUI();
  536. linkUIFeature._removeFormView();
  537. // Removing the form would call the focus spy.
  538. focusEditableSpy.resetHistory();
  539. actionsView.keystrokes.press( keyEvtData );
  540. expect( balloon.visibleView ).to.equal( null );
  541. expect( focusEditableSpy.calledOnce ).to.be.true;
  542. } );
  543. // #https://github.com/ckeditor/ckeditor5-link/issues/181
  544. it( 'should add the #formView upon Ctrl+K keystroke press', () => {
  545. const keyEvtData = {
  546. keyCode: keyCodes.k,
  547. ctrlKey: true,
  548. preventDefault: sinon.spy(),
  549. stopPropagation: sinon.spy()
  550. };
  551. linkUIFeature._showUI();
  552. linkUIFeature._removeFormView();
  553. expect( balloon.visibleView ).to.equal( actionsView );
  554. actionsView.keystrokes.press( keyEvtData );
  555. expect( balloon.visibleView ).to.equal( formView );
  556. } );
  557. } );
  558. } );
  559. describe( 'link form view', () => {
  560. let focusEditableSpy;
  561. beforeEach( () => {
  562. focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
  563. } );
  564. it( 'should mark the editor UI as focused when the #formView is focused', () => {
  565. linkUIFeature._showUI();
  566. expect( balloon.visibleView ).to.equal( formView );
  567. editor.ui.focusTracker.isFocused = false;
  568. formView.element.dispatchEvent( new Event( 'focus' ) );
  569. expect( editor.ui.focusTracker.isFocused ).to.be.true;
  570. } );
  571. describe( 'binding', () => {
  572. beforeEach( () => {
  573. setModelData( editor.model, '<paragraph>f[o]o</paragraph>' );
  574. } );
  575. it( 'should bind formView.urlInputView#value to link command value', () => {
  576. const command = editor.commands.get( 'link' );
  577. expect( formView.urlInputView.value ).to.undefined;
  578. command.value = 'http://cksource.com';
  579. expect( formView.urlInputView.value ).to.equal( 'http://cksource.com' );
  580. } );
  581. it( 'should execute link command on formView#submit event', () => {
  582. const executeSpy = testUtils.sinon.spy( editor, 'execute' );
  583. formView.urlInputView.value = 'http://ckeditor.com';
  584. expect( formView.urlInputView.inputView.element.value ).to.equal( 'http://ckeditor.com' );
  585. formView.urlInputView.inputView.element.value = 'http://cksource.com';
  586. formView.fire( 'submit' );
  587. expect( executeSpy.calledOnce ).to.be.true;
  588. expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com' ) ).to.be.true;
  589. } );
  590. it( 'should hide and reveal the #actionsView on formView#submit event', () => {
  591. linkUIFeature._showUI();
  592. formView.fire( 'submit' );
  593. expect( balloon.visibleView ).to.equal( actionsView );
  594. expect( focusEditableSpy.calledOnce ).to.be.true;
  595. } );
  596. it( 'should hide and reveal the #actionsView on formView#cancel event', () => {
  597. linkUIFeature._showUI();
  598. formView.fire( 'cancel' );
  599. expect( balloon.visibleView ).to.equal( actionsView );
  600. expect( focusEditableSpy.calledOnce ).to.be.true;
  601. } );
  602. it( 'should hide after Esc key press', () => {
  603. const keyEvtData = {
  604. keyCode: keyCodes.esc,
  605. preventDefault: sinon.spy(),
  606. stopPropagation: sinon.spy()
  607. };
  608. linkUIFeature._showUI();
  609. formView.keystrokes.press( keyEvtData );
  610. expect( balloon.visibleView ).to.equal( actionsView );
  611. expect( focusEditableSpy.calledOnce ).to.be.true;
  612. } );
  613. } );
  614. } );
  615. } );