8
0

mentionui.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* global window, document, setTimeout, Event */
  6. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  7. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  8. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  9. import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
  10. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  11. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  12. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  13. import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  14. import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  15. import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
  16. import MentionUI from '../src/mentionui';
  17. import MentionEditing from '../src/mentionediting';
  18. import MentionsView from '../src/ui/mentionsview';
  19. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  20. describe( 'MentionUI', () => {
  21. let editor, model, doc, editingView, mentionUI, editorElement, mentionsView, panelView;
  22. const staticConfig = {
  23. feeds: [
  24. {
  25. feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ],
  26. marker: '@'
  27. }
  28. ]
  29. };
  30. testUtils.createSinonSandbox();
  31. beforeEach( () => {
  32. editorElement = document.createElement( 'div' );
  33. document.body.appendChild( editorElement );
  34. } );
  35. afterEach( () => {
  36. sinon.restore();
  37. editorElement.remove();
  38. if ( editor ) {
  39. return editor.destroy();
  40. }
  41. } );
  42. it( 'should create a plugin instance', () => {
  43. return createClassicTestEditor().then( () => {
  44. expect( mentionUI ).to.instanceOf( Plugin );
  45. expect( mentionUI ).to.instanceOf( MentionUI );
  46. } );
  47. } );
  48. describe( 'init()', () => {
  49. it( 'should throw if marker was not provided for feed', () => {
  50. return createClassicTestEditor( { feeds: [ { feed: [ 'a' ] } ] } ).catch( error => {
  51. expect( error ).to.be.instanceOf( CKEditorError );
  52. expect( error.message ).to.match( /mentionconfig-incorrect-marker/ );
  53. } );
  54. } );
  55. it( 'should throw if marker is empty string', () => {
  56. return createClassicTestEditor( { feeds: [ { marker: '', feed: [ 'a' ] } ] } ).catch( error => {
  57. expect( error ).to.be.instanceOf( CKEditorError );
  58. expect( error.message ).to.match( /mentionconfig-incorrect-marker/ );
  59. } );
  60. } );
  61. it( 'should throw if marker is longer then 1 character', () => {
  62. return createClassicTestEditor( { feeds: [ { marker: '$$', feed: [ 'a' ] } ] } ).catch( error => {
  63. expect( error ).to.be.instanceOf( CKEditorError );
  64. expect( error.message ).to.match( /mentionconfig-incorrect-marker/ );
  65. } );
  66. } );
  67. } );
  68. describe( 'pluginName', () => {
  69. it( 'should return plugin by its name', () => {
  70. return createClassicTestEditor().then( () => {
  71. expect( editor.plugins.get( 'MentionUI' ) ).to.equal( mentionUI );
  72. } );
  73. } );
  74. } );
  75. describe( 'child views', () => {
  76. beforeEach( () => createClassicTestEditor() );
  77. describe( 'panelView', () => {
  78. it( 'should create a view instance', () => {
  79. expect( panelView ).to.instanceof( BalloonPanelView );
  80. } );
  81. it( 'should be added to the ui.view.body collection', () => {
  82. expect( Array.from( editor.ui.view.body ) ).to.include( panelView );
  83. } );
  84. it( 'should have disabled arrow', () => {
  85. expect( panelView.withArrow ).to.be.false;
  86. } );
  87. it( 'should have added MentionView as a child', () => {
  88. expect( panelView.content.get( 0 ) ).to.be.instanceof( MentionsView );
  89. } );
  90. } );
  91. } );
  92. describe( 'position', () => {
  93. let pinSpy;
  94. const caretRect = {
  95. bottom: 118,
  96. height: 18,
  97. left: 500,
  98. right: 501,
  99. top: 100,
  100. width: 1
  101. };
  102. const balloonRect = {
  103. bottom: 150,
  104. height: 150,
  105. left: 0,
  106. right: 200,
  107. top: 0,
  108. width: 200
  109. };
  110. beforeEach( () => {
  111. return createClassicTestEditor( staticConfig ).then( () => {
  112. pinSpy = sinon.spy( panelView, 'pin' );
  113. } );
  114. } );
  115. it( 'should properly calculate position data', () => {
  116. const editableElement = editingView.document.selection.editableElement;
  117. setData( model, '<paragraph>foo []</paragraph>' );
  118. stubSelectionRects( [ caretRect ] );
  119. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  120. model.change( writer => {
  121. writer.insertText( '@', doc.selection.getFirstPosition() );
  122. } );
  123. return waitForDebounce()
  124. .then( () => {
  125. const pinArgument = pinSpy.firstCall.args[ 0 ];
  126. const { target, positions, limiter, fitInViewport } = pinArgument;
  127. expect( fitInViewport ).to.be.true;
  128. expect( positions ).to.have.length( 4 );
  129. // Mention UI should set limiter to the editable area.
  130. expect( limiter() ).to.equal( editingView.domConverter.mapViewToDom( editableElement ) );
  131. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  132. const mentionMarker = editor.model.markers.get( 'mention' );
  133. const focus = doc.selection.focus;
  134. const expectedRange = editor.model.createRange( focus.getShiftedBy( -1 ), focus );
  135. // It should create a model marker for matcher marker character ('@').
  136. expect( expectedRange.isEqual( mentionMarker.getRange() ) ).to.be.true;
  137. const toViewRangeSpy = sinon.spy( editor.editing.mapper, 'toViewRange' );
  138. expect( target() ).to.deep.equal( caretRect );
  139. sinon.assert.calledOnce( toViewRangeSpy );
  140. const range = toViewRangeSpy.firstCall.args[ 0 ];
  141. expect( mentionMarker.getRange().isEqual( range ), 'Should position to mention marker.' );
  142. const caretSouthEast = positions[ 0 ];
  143. const caretNorthEast = positions[ 1 ];
  144. const caretSouthWest = positions[ 2 ];
  145. const caretNorthWest = positions[ 3 ];
  146. expect( caretSouthEast( caretRect, balloonRect ) ).to.deep.equal( {
  147. left: 501,
  148. name: 'caret_se',
  149. top: 121
  150. } );
  151. expect( caretNorthEast( caretRect, balloonRect ) ).to.deep.equal( {
  152. left: 501,
  153. name: 'caret_ne',
  154. top: -53
  155. } );
  156. expect( caretSouthWest( caretRect, balloonRect ) ).to.deep.equal( {
  157. left: 301,
  158. name: 'caret_sw',
  159. top: 121
  160. } );
  161. expect( caretNorthWest( caretRect, balloonRect ) ).to.deep.equal( {
  162. left: 301,
  163. name: 'caret_nw',
  164. top: -53
  165. } );
  166. } );
  167. } );
  168. it( 'should re-calculate position on typing and stay on selected position', () => {
  169. setData( model, '<paragraph>foo []</paragraph>' );
  170. stubSelectionRects( [ caretRect ] );
  171. model.change( writer => {
  172. writer.insertText( '@', doc.selection.getFirstPosition() );
  173. } );
  174. let positionAfterFirstShow;
  175. return waitForDebounce()
  176. .then( () => {
  177. sinon.assert.calledOnce( pinSpy );
  178. const pinArgument = pinSpy.firstCall.args[ 0 ];
  179. const { positions } = pinArgument;
  180. expect( positions ).to.have.length( 4 );
  181. positionAfterFirstShow = panelView.position;
  182. model.change( writer => {
  183. writer.insertText( 't', doc.selection.getFirstPosition() );
  184. } );
  185. } )
  186. .then( waitForDebounce )
  187. .then( () => {
  188. sinon.assert.calledTwice( pinSpy );
  189. const pinArgument = pinSpy.secondCall.args[ 0 ];
  190. const { positions } = pinArgument;
  191. expect( positions, 'should reuse first matched position' ).to.have.length( 1 );
  192. expect( positions[ 0 ].name ).to.equal( positionAfterFirstShow );
  193. } );
  194. } );
  195. it( 'does not fail if selection has no #editableElement', () => {
  196. setData( model, '<paragraph>foo []</paragraph>' );
  197. stubSelectionRects( [ caretRect ] );
  198. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  199. model.change( writer => {
  200. writer.insertText( '@', doc.selection.getFirstPosition() );
  201. } );
  202. return waitForDebounce()
  203. .then( () => {
  204. const pinArgument = pinSpy.firstCall.args[ 0 ];
  205. const { limiter } = pinArgument;
  206. sinon.stub( editingView.document.selection, 'editableElement' ).value( null );
  207. // Should not break;
  208. expect( limiter() ).to.be.null;
  209. } );
  210. } );
  211. } );
  212. describe( 'typing integration', () => {
  213. it( 'should show panel for matched marker after typing minimum characters', () => {
  214. return createClassicTestEditor( { feeds: [ Object.assign( { minimumCharacters: 2 }, staticConfig.feeds[ 0 ] ) ] } )
  215. .then( () => {
  216. setData( model, '<paragraph>foo []</paragraph>' );
  217. model.change( writer => {
  218. writer.insertText( '@', doc.selection.getFirstPosition() );
  219. } );
  220. } )
  221. .then( () => {
  222. model.change( writer => {
  223. writer.insertText( 'B', doc.selection.getFirstPosition() );
  224. } );
  225. } )
  226. .then( waitForDebounce )
  227. .then( () => {
  228. expect( panelView.isVisible ).to.be.false;
  229. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  230. } )
  231. .then( waitForDebounce )
  232. .then( () => {
  233. model.change( writer => {
  234. writer.insertText( 'a', doc.selection.getFirstPosition() );
  235. } );
  236. } )
  237. .then( waitForDebounce )
  238. .then( () => {
  239. expect( panelView.isVisible ).to.be.true;
  240. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  241. expect( mentionsView.items ).to.have.length( 1 );
  242. model.change( writer => {
  243. writer.insertText( 'r', doc.selection.getFirstPosition() );
  244. } );
  245. } )
  246. .then( waitForDebounce )
  247. .then( () => {
  248. expect( panelView.isVisible ).to.be.true;
  249. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  250. expect( mentionsView.items ).to.have.length( 1 );
  251. } );
  252. } );
  253. describe( 'static list with large set of results', () => {
  254. const bigList = {
  255. marker: '@',
  256. feed: [
  257. '@a01', '@a02', '@a03', '@a04', '@a05', '@a06', '@a07', '@a08', '@a09', '@a10', '@a11', '@a12'
  258. ]
  259. };
  260. beforeEach( () => {
  261. return createClassicTestEditor( { feeds: [ bigList ] } );
  262. } );
  263. it( 'should show panel with no more then 10 items for default static feed', () => {
  264. setData( model, '<paragraph>foo []</paragraph>' );
  265. model.change( writer => {
  266. writer.insertText( '@', doc.selection.getFirstPosition() );
  267. } );
  268. return waitForDebounce()
  269. .then( () => {
  270. expect( panelView.isVisible ).to.be.true;
  271. expect( mentionsView.items ).to.have.length( 10 );
  272. } );
  273. } );
  274. it( 'should scroll mention panel to the selected item', () => {
  275. setData( model, '<paragraph>foo []</paragraph>' );
  276. model.change( writer => {
  277. writer.insertText( '@', doc.selection.getFirstPosition() );
  278. } );
  279. return waitForDebounce()
  280. .then( () => {
  281. expect( panelView.isVisible ).to.be.true;
  282. expectChildViewsIsOnState( [ true, false, false, false, false, false, false, false, false, false ] );
  283. const arrowDownEvtData = {
  284. keyCode: keyCodes.arrowdown,
  285. preventDefault: sinon.spy(),
  286. stopPropagation: sinon.spy()
  287. };
  288. const arrowUpEvtData = {
  289. keyCode: keyCodes.arrowup,
  290. preventDefault: sinon.spy(),
  291. stopPropagation: sinon.spy()
  292. };
  293. fireKeyDownEvent( arrowDownEvtData );
  294. expect( mentionsView.element.scrollTop ).to.equal( 0 );
  295. expectChildViewsIsOnState( [ false, true, false, false, false, false, false, false, false, false ] );
  296. fireKeyDownEvent( arrowUpEvtData );
  297. fireKeyDownEvent( arrowUpEvtData );
  298. expectChildViewsIsOnState( [ false, false, false, false, false, false, false, false, false, true ] );
  299. expect( mentionsView.element.scrollTop ).to.be.not.equal( 0 );
  300. fireKeyDownEvent( arrowDownEvtData );
  301. expectChildViewsIsOnState( [ true, false, false, false, false, false, false, false, false, false ] );
  302. expect( mentionsView.element.scrollTop ).to.equal( 0 );
  303. } );
  304. } );
  305. } );
  306. describe( 'static list with default trigger', () => {
  307. beforeEach( () => {
  308. return createClassicTestEditor( staticConfig );
  309. } );
  310. it( 'should show panel for matched marker', () => {
  311. setData( model, '<paragraph>foo []</paragraph>' );
  312. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  313. model.change( writer => {
  314. writer.insertText( '@', doc.selection.getFirstPosition() );
  315. } );
  316. return waitForDebounce()
  317. .then( () => {
  318. expect( panelView.isVisible ).to.be.true;
  319. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  320. expect( mentionsView.items ).to.have.length( 5 );
  321. } );
  322. } );
  323. it( 'should show panel for matched marker at the beginning of paragraph', () => {
  324. setData( model, '<paragraph>[] foo</paragraph>' );
  325. model.change( writer => {
  326. writer.insertText( '@', doc.selection.getFirstPosition() );
  327. } );
  328. return waitForDebounce()
  329. .then( () => {
  330. expect( panelView.isVisible ).to.be.true;
  331. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  332. expect( mentionsView.items ).to.have.length( 5 );
  333. } );
  334. } );
  335. it( 'should not show panel for marker in the middle of other word', () => {
  336. setData( model, '<paragraph>foo[]</paragraph>' );
  337. model.change( writer => {
  338. writer.insertText( '@', doc.selection.getFirstPosition() );
  339. } );
  340. return waitForDebounce()
  341. .then( () => {
  342. expect( panelView.isVisible ).to.be.false;
  343. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  344. } );
  345. } );
  346. it( 'should not show panel when selection is inside a mention', () => {
  347. setData( model, '<paragraph>foo [@John] bar</paragraph>' );
  348. model.change( writer => {
  349. writer.setAttribute( 'mention', { id: '@John', _uid: 1234 }, doc.selection.getFirstRange() );
  350. } );
  351. model.change( writer => {
  352. writer.setSelection( doc.getRoot().getChild( 0 ), 7 );
  353. } );
  354. return waitForDebounce()
  355. .then( () => {
  356. expect( panelView.isVisible ).to.be.false;
  357. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  358. } );
  359. } );
  360. it( 'should not show panel when selection is at the end of a mention', () => {
  361. setData( model, '<paragraph>foo [@John] bar</paragraph>' );
  362. model.change( writer => {
  363. writer.setAttribute( 'mention', { id: '@John', _uid: 1234 }, doc.selection.getFirstRange() );
  364. } );
  365. model.change( writer => {
  366. writer.setSelection( doc.getRoot().getChild( 0 ), 9 );
  367. } );
  368. return waitForDebounce()
  369. .then( () => {
  370. expect( panelView.isVisible ).to.be.false;
  371. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  372. } );
  373. } );
  374. it( 'should not show panel when selection is not collapsed', () => {
  375. setData( model, '<paragraph>foo []</paragraph>' );
  376. model.change( writer => {
  377. writer.insertText( '@', doc.selection.getFirstPosition() );
  378. } );
  379. return waitForDebounce()
  380. .then( () => {
  381. expect( panelView.isVisible ).to.be.true;
  382. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  383. model.change( () => {
  384. model.modifySelection( doc.selection, { direction: 'backward', unit: 'character' } );
  385. } );
  386. } )
  387. .then( waitForDebounce )
  388. .then( () => {
  389. expect( panelView.isVisible ).to.be.false;
  390. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  391. } );
  392. } );
  393. it( 'should not show panel when selection is changing (non-collapsed)', () => {
  394. setData( model, '<paragraph>foo []</paragraph>' );
  395. model.change( writer => {
  396. writer.insertText( '@', doc.selection.getFirstPosition() );
  397. } );
  398. return waitForDebounce()
  399. .then( () => {
  400. expect( panelView.isVisible ).to.be.true;
  401. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  402. model.change( () => {
  403. model.modifySelection( doc.selection, { direction: 'backward', unit: 'character' } );
  404. } );
  405. } )
  406. .then( waitForDebounce )
  407. .then( () => {
  408. expect( panelView.isVisible ).to.be.false;
  409. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  410. } )
  411. .then( () => {
  412. model.change( () => {
  413. model.modifySelection( doc.selection, { direction: 'backward', unit: 'character' } );
  414. } );
  415. } )
  416. .then( waitForDebounce )
  417. .then( () => {
  418. expect( panelView.isVisible ).to.be.false;
  419. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  420. } );
  421. } );
  422. it( 'should not show panel when selection is after existing mention', () => {
  423. setData( model, '<paragraph>foo [@John] bar[]</paragraph>' );
  424. model.change( writer => {
  425. writer.setAttribute( 'mention', { id: '@John', _uid: 1234 }, doc.selection.getFirstRange() );
  426. } );
  427. return waitForDebounce()
  428. .then( () => {
  429. expect( panelView.isVisible ).to.be.false;
  430. model.change( writer => {
  431. writer.setSelection( doc.getRoot().getChild( 0 ), 8 );
  432. } );
  433. } )
  434. .then( waitForDebounce )
  435. .then( () => {
  436. expect( panelView.isVisible ).to.be.false;
  437. } );
  438. } );
  439. it( 'should show filtered results for matched text', () => {
  440. setData( model, '<paragraph>foo []</paragraph>' );
  441. model.change( writer => {
  442. writer.insertText( '@', doc.selection.getFirstPosition() );
  443. } );
  444. model.change( writer => {
  445. writer.insertText( 'T', doc.selection.getFirstPosition() );
  446. } );
  447. return waitForDebounce()
  448. .then( () => {
  449. expect( panelView.isVisible ).to.be.true;
  450. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  451. expect( mentionsView.items ).to.have.length( 1 );
  452. } );
  453. } );
  454. it( 'should focus the first item in panel', () => {
  455. setData( model, '<paragraph>foo []</paragraph>' );
  456. model.change( writer => {
  457. writer.insertText( '@', doc.selection.getFirstPosition() );
  458. } );
  459. return waitForDebounce()
  460. .then( () => {
  461. const button = mentionsView.items.get( 0 ).children.get( 0 );
  462. expect( button.isOn ).to.be.true;
  463. } );
  464. } );
  465. it( 'should hide panel if no matched items', () => {
  466. setData( model, '<paragraph>foo []</paragraph>' );
  467. model.change( writer => {
  468. writer.insertText( '@', doc.selection.getFirstPosition() );
  469. } );
  470. return waitForDebounce()
  471. .then( () => expect( panelView.isVisible ).to.be.true )
  472. .then( () => {
  473. model.change( writer => {
  474. writer.insertText( 'x', doc.selection.getFirstPosition() );
  475. } );
  476. } )
  477. .then( waitForDebounce )
  478. .then( () => {
  479. expect( panelView.isVisible ).to.be.false;
  480. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  481. expect( mentionsView.items ).to.have.length( 0 );
  482. } );
  483. } );
  484. it( 'should hide panel when text was unmatched', () => {
  485. setData( model, '<paragraph>foo []</paragraph>' );
  486. model.change( writer => {
  487. writer.insertText( '@', doc.selection.getFirstPosition() );
  488. } );
  489. return waitForDebounce()
  490. .then( () => expect( panelView.isVisible ).to.be.true )
  491. .then( () => {
  492. model.change( writer => {
  493. const end = doc.selection.getFirstPosition();
  494. const start = end.getShiftedBy( -1 );
  495. writer.remove( writer.createRange( start, end ) );
  496. } );
  497. } )
  498. .then( waitForDebounce )
  499. .then( () => expect( panelView.isVisible ).to.be.false );
  500. } );
  501. } );
  502. describe( 'asynchronous list with custom trigger', () => {
  503. beforeEach( () => {
  504. const issuesNumbers = [ '#100', '#101', '#102', '#103' ];
  505. return createClassicTestEditor( {
  506. feeds: [
  507. {
  508. marker: '#',
  509. feed: feedText => {
  510. return new Promise( resolve => {
  511. setTimeout( () => {
  512. resolve( issuesNumbers.filter( number => number.includes( feedText ) ) );
  513. }, 20 );
  514. } );
  515. }
  516. }
  517. ]
  518. } );
  519. } );
  520. it( 'should show panel for matched marker', () => {
  521. setData( model, '<paragraph>foo []</paragraph>' );
  522. model.change( writer => {
  523. writer.insertText( '#', doc.selection.getFirstPosition() );
  524. } );
  525. return waitForDebounce()
  526. .then( () => {
  527. expect( panelView.isVisible ).to.be.true;
  528. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  529. expect( mentionsView.items ).to.have.length( 4 );
  530. } );
  531. } );
  532. it( 'should show filtered results for matched text', () => {
  533. setData( model, '<paragraph>foo []</paragraph>' );
  534. model.change( writer => {
  535. writer.insertText( '#', doc.selection.getFirstPosition() );
  536. } );
  537. model.change( writer => {
  538. writer.insertText( '2', doc.selection.getFirstPosition() );
  539. } );
  540. return waitForDebounce()
  541. .then( () => {
  542. expect( panelView.isVisible ).to.be.true;
  543. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  544. expect( mentionsView.items ).to.have.length( 1 );
  545. } );
  546. } );
  547. it( 'should hide panel if no matched items', () => {
  548. setData( model, '<paragraph>foo []</paragraph>' );
  549. model.change( writer => {
  550. writer.insertText( '#', doc.selection.getFirstPosition() );
  551. } );
  552. return waitForDebounce()
  553. .then( () => expect( panelView.isVisible ).to.be.true )
  554. .then( () => {
  555. model.change( writer => {
  556. writer.insertText( 'x', doc.selection.getFirstPosition() );
  557. } );
  558. } )
  559. .then( waitForDebounce )
  560. .then( () => {
  561. expect( panelView.isVisible ).to.be.false;
  562. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  563. expect( mentionsView.items ).to.have.length( 0 );
  564. } );
  565. } );
  566. it( 'should hide panel when text was unmatched', () => {
  567. setData( model, '<paragraph>foo []</paragraph>' );
  568. model.change( writer => {
  569. writer.insertText( '#', doc.selection.getFirstPosition() );
  570. } );
  571. return waitForDebounce()
  572. .then( () => expect( panelView.isVisible ).to.be.true )
  573. .then( () => {
  574. model.change( writer => {
  575. const end = doc.selection.getFirstPosition();
  576. const start = end.getShiftedBy( -1 );
  577. writer.remove( writer.createRange( start, end ) );
  578. } );
  579. } )
  580. .then( waitForDebounce )
  581. .then( () => expect( panelView.isVisible ).to.be.false );
  582. } );
  583. } );
  584. } );
  585. describe( 'panel behavior', () => {
  586. it( 'should close the opened panel on esc', () => {
  587. return createClassicTestEditor( staticConfig )
  588. .then( () => {
  589. setData( model, '<paragraph>foo []</paragraph>' );
  590. model.change( writer => {
  591. writer.insertText( '@', doc.selection.getFirstPosition() );
  592. } );
  593. } )
  594. .then( waitForDebounce )
  595. .then( () => {
  596. expect( panelView.isVisible ).to.be.true;
  597. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  598. fireKeyDownEvent( {
  599. keyCode: keyCodes.esc,
  600. preventDefault: sinon.spy(),
  601. stopPropagation: sinon.spy()
  602. } );
  603. expect( panelView.isVisible ).to.be.false;
  604. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  605. } );
  606. } );
  607. it( 'should close the opened panel when click outside the panel', () => {
  608. return createClassicTestEditor( staticConfig )
  609. .then( () => {
  610. setData( model, '<paragraph>foo []</paragraph>' );
  611. model.change( writer => {
  612. writer.insertText( '@', doc.selection.getFirstPosition() );
  613. } );
  614. } )
  615. .then( waitForDebounce )
  616. .then( () => {
  617. expect( panelView.isVisible ).to.be.true;
  618. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  619. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  620. expect( panelView.isVisible ).to.be.false;
  621. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  622. } );
  623. } );
  624. it( 'should hide the panel on selection change', () => {
  625. return createClassicTestEditor( staticConfig )
  626. .then( () => {
  627. setData( model, '<paragraph>foo []</paragraph>' );
  628. model.change( writer => {
  629. writer.insertText( '@', doc.selection.getFirstPosition() );
  630. } );
  631. } )
  632. .then( waitForDebounce )
  633. .then( () => {
  634. expect( panelView.isVisible ).to.be.true;
  635. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  636. model.change( writer => {
  637. // Place position at the beginning of a paragraph.
  638. writer.setSelection( doc.getRoot().getChild( 0 ), 0 );
  639. } );
  640. expect( panelView.isVisible ).to.be.false;
  641. expect( panelView.position ).to.be.undefined;
  642. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  643. } );
  644. } );
  645. it( 'should hide the panel on selection change triggered by mouse click', () => {
  646. return createClassicTestEditor( staticConfig )
  647. .then( () => {
  648. setData( model, '<paragraph>foo []</paragraph>' );
  649. model.change( writer => {
  650. writer.insertText( '@', doc.selection.getFirstPosition() );
  651. } );
  652. } )
  653. .then( waitForDebounce )
  654. .then( () => {
  655. expect( panelView.isVisible ).to.be.true;
  656. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  657. // This happens when user clicks outside the panel view and selection is changed.
  658. // Two panel closing mechanisms are run:
  659. // - clickOutsideHandler
  660. // - unmatched text in text watcher
  661. // which may fail when trying to remove mention marker twice.
  662. document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
  663. model.change( writer => {
  664. // Place position at the beginning of a paragraph.
  665. writer.setSelection( doc.getRoot().getChild( 0 ), 0 );
  666. } );
  667. expect( panelView.isVisible ).to.be.false;
  668. expect( panelView.position ).to.be.undefined;
  669. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  670. } );
  671. } );
  672. describe( 'default list item', () => {
  673. // Create map of expected feed items as objects as they will be stored internally.
  674. const feedItems = staticConfig.feeds[ 0 ].feed.map( text => ( { text: `${ text }`, id: `${ text }` } ) );
  675. beforeEach( () => {
  676. return createClassicTestEditor( staticConfig );
  677. } );
  678. describe( 'on arrows', () => {
  679. it( 'should cycle down on arrow down', () => {
  680. setData( model, '<paragraph>foo []</paragraph>' );
  681. model.change( writer => {
  682. writer.insertText( '@', doc.selection.getFirstPosition() );
  683. } );
  684. return waitForDebounce()
  685. .then( () => {
  686. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  687. const keyEvtData = {
  688. keyCode: keyCodes.arrowdown,
  689. preventDefault: sinon.spy(),
  690. stopPropagation: sinon.spy()
  691. };
  692. fireKeyDownEvent( keyEvtData );
  693. expectChildViewsIsOnState( [ false, true, false, false, false ] );
  694. fireKeyDownEvent( keyEvtData );
  695. expectChildViewsIsOnState( [ false, false, true, false, false ] );
  696. fireKeyDownEvent( keyEvtData );
  697. expectChildViewsIsOnState( [ false, false, false, true, false ] );
  698. fireKeyDownEvent( keyEvtData );
  699. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  700. fireKeyDownEvent( keyEvtData );
  701. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  702. } );
  703. } );
  704. it( 'should cycle up on arrow up', () => {
  705. setData( model, '<paragraph>foo []</paragraph>' );
  706. model.change( writer => {
  707. writer.insertText( '@', doc.selection.getFirstPosition() );
  708. } );
  709. return waitForDebounce()
  710. .then( () => {
  711. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  712. const keyEvtData = {
  713. keyCode: keyCodes.arrowup,
  714. preventDefault: sinon.spy(),
  715. stopPropagation: sinon.spy()
  716. };
  717. fireKeyDownEvent( keyEvtData );
  718. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  719. fireKeyDownEvent( keyEvtData );
  720. expectChildViewsIsOnState( [ false, false, false, true, false ] );
  721. fireKeyDownEvent( keyEvtData );
  722. expectChildViewsIsOnState( [ false, false, true, false, false ] );
  723. fireKeyDownEvent( keyEvtData );
  724. expectChildViewsIsOnState( [ false, true, false, false, false ] );
  725. fireKeyDownEvent( keyEvtData );
  726. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  727. } );
  728. } );
  729. } );
  730. describe( 'on "execute" keys', () => {
  731. testExecuteKey( 'enter', keyCodes.enter, feedItems );
  732. testExecuteKey( 'tab', keyCodes.tab, feedItems );
  733. testExecuteKey( 'space', keyCodes.space, feedItems );
  734. } );
  735. } );
  736. describe( 'default list item with custom feed', () => {
  737. const issues = [
  738. { id: '@Ted' },
  739. { id: '@Barney' },
  740. { id: '@Robin' },
  741. { id: '@Lily' },
  742. { id: '@Marshal' }
  743. ];
  744. beforeEach( () => {
  745. return createClassicTestEditor( {
  746. feeds: [
  747. {
  748. marker: '@',
  749. feed: feedText => issues.filter( issue => issue.id.includes( feedText ) )
  750. }
  751. ]
  752. } );
  753. } );
  754. it( 'should show panel for matched marker', () => {
  755. setData( model, '<paragraph>foo []</paragraph>' );
  756. model.change( writer => {
  757. writer.insertText( '@', doc.selection.getFirstPosition() );
  758. } );
  759. return waitForDebounce()
  760. .then( () => {
  761. expect( panelView.isVisible ).to.be.true;
  762. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  763. expect( mentionsView.items ).to.have.length( 5 );
  764. } );
  765. } );
  766. } );
  767. describe( 'custom list item (string)', () => {
  768. const issues = [
  769. { id: '@1002', title: 'Some bug in editor.' },
  770. { id: '@1003', title: 'Introduce this feature.' },
  771. { id: '@1004', title: 'Missing docs.' },
  772. { id: '@1005', title: 'Another bug.' },
  773. { id: '@1006', title: 'More bugs' }
  774. ];
  775. beforeEach( () => {
  776. return createClassicTestEditor( {
  777. feeds: [
  778. {
  779. marker: '@',
  780. feed: issues,
  781. itemRenderer: item => item.title
  782. }
  783. ]
  784. } );
  785. } );
  786. it( 'should show panel for matched marker', () => {
  787. setData( model, '<paragraph>foo []</paragraph>' );
  788. model.change( writer => {
  789. writer.insertText( '@', doc.selection.getFirstPosition() );
  790. } );
  791. return waitForDebounce()
  792. .then( () => {
  793. expect( panelView.isVisible ).to.be.true;
  794. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  795. expect( mentionsView.items ).to.have.length( 5 );
  796. } );
  797. } );
  798. describe( 'keys', () => {
  799. describe( 'on arrows', () => {
  800. it( 'should cycle down on arrow down', () => {
  801. setData( model, '<paragraph>foo []</paragraph>' );
  802. model.change( writer => {
  803. writer.insertText( '@', doc.selection.getFirstPosition() );
  804. } );
  805. return waitForDebounce()
  806. .then( () => {
  807. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  808. const keyEvtData = {
  809. keyCode: keyCodes.arrowdown,
  810. preventDefault: sinon.spy(),
  811. stopPropagation: sinon.spy()
  812. };
  813. fireKeyDownEvent( keyEvtData );
  814. expectChildViewsIsOnState( [ false, true, false, false, false ] );
  815. fireKeyDownEvent( keyEvtData );
  816. expectChildViewsIsOnState( [ false, false, true, false, false ] );
  817. fireKeyDownEvent( keyEvtData );
  818. expectChildViewsIsOnState( [ false, false, false, true, false ] );
  819. fireKeyDownEvent( keyEvtData );
  820. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  821. fireKeyDownEvent( keyEvtData );
  822. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  823. } );
  824. } );
  825. it( 'should cycle up on arrow up', () => {
  826. setData( model, '<paragraph>foo []</paragraph>' );
  827. model.change( writer => {
  828. writer.insertText( '@', doc.selection.getFirstPosition() );
  829. } );
  830. return waitForDebounce()
  831. .then( () => {
  832. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  833. const keyEvtData = {
  834. keyCode: keyCodes.arrowup,
  835. preventDefault: sinon.spy(),
  836. stopPropagation: sinon.spy()
  837. };
  838. fireKeyDownEvent( keyEvtData );
  839. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  840. fireKeyDownEvent( keyEvtData );
  841. expectChildViewsIsOnState( [ false, false, false, true, false ] );
  842. fireKeyDownEvent( keyEvtData );
  843. expectChildViewsIsOnState( [ false, false, true, false, false ] );
  844. fireKeyDownEvent( keyEvtData );
  845. expectChildViewsIsOnState( [ false, true, false, false, false ] );
  846. fireKeyDownEvent( keyEvtData );
  847. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  848. } );
  849. } );
  850. } );
  851. describe( 'on "execute" keys', () => {
  852. testExecuteKey( 'enter', keyCodes.enter, issues );
  853. testExecuteKey( 'tab', keyCodes.tab, issues );
  854. testExecuteKey( 'space', keyCodes.space, issues );
  855. } );
  856. } );
  857. } );
  858. describe( 'custom list item (DOM Element)', () => {
  859. const issues = [
  860. { id: '@1002', title: 'Some bug in editor.' },
  861. { id: '@1003', title: 'Introduce this feature.' },
  862. { id: '@1004', title: 'Missing docs.' },
  863. { id: '@1005', title: 'Another bug.' },
  864. { id: '@1006', title: 'More bugs' }
  865. ];
  866. beforeEach( () => {
  867. return createClassicTestEditor( {
  868. feeds: [
  869. {
  870. marker: '@',
  871. feed: feedText => {
  872. return Promise.resolve( issues.filter( issue => issue.id.includes( feedText ) ) );
  873. },
  874. itemRenderer: item => {
  875. const span = global.document.createElementNS( 'http://www.w3.org/1999/xhtml', 'span' );
  876. span.innerHTML = `<span id="issue-${ item.id.slice( 1 ) }">@${ item.title }</span>`;
  877. return span;
  878. }
  879. }
  880. ]
  881. } );
  882. } );
  883. it( 'should show panel for matched marker', () => {
  884. setData( model, '<paragraph>foo []</paragraph>' );
  885. model.change( writer => {
  886. writer.insertText( '@', doc.selection.getFirstPosition() );
  887. } );
  888. return waitForDebounce()
  889. .then( () => {
  890. expect( panelView.isVisible ).to.be.true;
  891. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  892. expect( mentionsView.items ).to.have.length( 5 );
  893. } );
  894. } );
  895. describe( 'keys', () => {
  896. describe( 'on arrows', () => {
  897. it( 'should cycle down on arrow down', () => {
  898. setData( model, '<paragraph>foo []</paragraph>' );
  899. model.change( writer => {
  900. writer.insertText( '@', doc.selection.getFirstPosition() );
  901. } );
  902. return waitForDebounce()
  903. .then( () => {
  904. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  905. const keyEvtData = {
  906. keyCode: keyCodes.arrowdown,
  907. preventDefault: sinon.spy(),
  908. stopPropagation: sinon.spy()
  909. };
  910. fireKeyDownEvent( keyEvtData );
  911. expectChildViewsIsOnState( [ false, true, false, false, false ] );
  912. fireKeyDownEvent( keyEvtData );
  913. expectChildViewsIsOnState( [ false, false, true, false, false ] );
  914. fireKeyDownEvent( keyEvtData );
  915. expectChildViewsIsOnState( [ false, false, false, true, false ] );
  916. fireKeyDownEvent( keyEvtData );
  917. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  918. fireKeyDownEvent( keyEvtData );
  919. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  920. } );
  921. } );
  922. it( 'should cycle up on arrow up', () => {
  923. setData( model, '<paragraph>foo []</paragraph>' );
  924. model.change( writer => {
  925. writer.insertText( '@', doc.selection.getFirstPosition() );
  926. } );
  927. return waitForDebounce()
  928. .then( () => {
  929. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  930. const keyEvtData = {
  931. keyCode: keyCodes.arrowup,
  932. preventDefault: sinon.spy(),
  933. stopPropagation: sinon.spy()
  934. };
  935. fireKeyDownEvent( keyEvtData );
  936. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  937. fireKeyDownEvent( keyEvtData );
  938. expectChildViewsIsOnState( [ false, false, false, true, false ] );
  939. fireKeyDownEvent( keyEvtData );
  940. expectChildViewsIsOnState( [ false, false, true, false, false ] );
  941. fireKeyDownEvent( keyEvtData );
  942. expectChildViewsIsOnState( [ false, true, false, false, false ] );
  943. fireKeyDownEvent( keyEvtData );
  944. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  945. } );
  946. } );
  947. } );
  948. describe( 'on "execute" keys', () => {
  949. testExecuteKey( 'enter', keyCodes.enter, issues );
  950. testExecuteKey( 'tab', keyCodes.tab, issues );
  951. testExecuteKey( 'space', keyCodes.space, issues );
  952. } );
  953. describe( 'mouse', () => {
  954. it( 'should execute selected button on mouse click', () => {
  955. setData( model, '<paragraph>foo []</paragraph>' );
  956. model.change( writer => {
  957. writer.insertText( '@', doc.selection.getFirstPosition() );
  958. } );
  959. const command = editor.commands.get( 'mention' );
  960. const spy = testUtils.sinon.spy( command, 'execute' );
  961. return waitForDebounce()
  962. .then( () => {
  963. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  964. const element = panelView.element.querySelector( '#issue-1004' );
  965. element.dispatchEvent( new Event( 'click', { bubbles: true } ) );
  966. sinon.assert.calledOnce( spy );
  967. const commandOptions = spy.getCall( 0 ).args[ 0 ];
  968. const item = issues[ 2 ];
  969. expect( commandOptions ).to.have.property( 'mention' ).that.deep.equal( item );
  970. expect( commandOptions ).to.have.property( 'marker', '@' );
  971. expect( commandOptions ).to.have.property( 'range' );
  972. const start = model.createPositionAt( doc.getRoot().getChild( 0 ), 4 );
  973. const expectedRange = model.createRange( start, start.getShiftedBy( 1 ) );
  974. expect( commandOptions.range.isEqual( expectedRange ) ).to.be.true;
  975. } );
  976. } );
  977. } );
  978. } );
  979. } );
  980. describe( 'multiple feeds configuration', () => {
  981. beforeEach( () => {
  982. return createClassicTestEditor( {
  983. feeds: [
  984. {
  985. marker: '@',
  986. feed: [ '@a1', '@a2', '@a3' ]
  987. },
  988. {
  989. marker: '$',
  990. feed: [ '$a1', '$a2', '$a3', '$a4', '$a5' ]
  991. }
  992. ]
  993. } );
  994. } );
  995. it( 'should show panel for matched marker', () => {
  996. setData( model, '<paragraph>foo []</paragraph>' );
  997. model.change( writer => {
  998. writer.insertText( '@', doc.selection.getFirstPosition() );
  999. } );
  1000. return waitForDebounce()
  1001. .then( () => {
  1002. expect( panelView.isVisible ).to.be.true;
  1003. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  1004. expect( mentionsView.items ).to.have.length( 3 );
  1005. mentionsView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
  1006. } )
  1007. .then( waitForDebounce )
  1008. .then( () => {
  1009. expect( panelView.isVisible ).to.be.false;
  1010. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  1011. model.change( writer => {
  1012. writer.insertText( '$', doc.selection.getFirstPosition() );
  1013. } );
  1014. } )
  1015. .then( waitForDebounce )
  1016. .then( () => {
  1017. expect( panelView.isVisible ).to.be.true;
  1018. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  1019. expect( mentionsView.items ).to.have.length( 5 );
  1020. mentionsView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
  1021. } )
  1022. .then( waitForDebounce )
  1023. .then( () => {
  1024. expect( panelView.isVisible ).to.be.false;
  1025. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  1026. model.change( writer => {
  1027. writer.insertText( '@', doc.selection.getFirstPosition() );
  1028. } );
  1029. } )
  1030. .then( waitForDebounce )
  1031. .then( () => {
  1032. expect( panelView.isVisible ).to.be.true;
  1033. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  1034. expect( mentionsView.items ).to.have.length( 3 );
  1035. } );
  1036. } );
  1037. } );
  1038. function testExecuteKey( name, keyCode, feedItems ) {
  1039. it( 'should execute selected button on ' + name, () => {
  1040. setData( model, '<paragraph>foo []</paragraph>' );
  1041. model.change( writer => {
  1042. writer.insertText( '@', doc.selection.getFirstPosition() );
  1043. } );
  1044. const command = editor.commands.get( 'mention' );
  1045. const spy = testUtils.sinon.spy( command, 'execute' );
  1046. return waitForDebounce()
  1047. .then( () => {
  1048. expectChildViewsIsOnState( [ true, false, false, false, false ] );
  1049. fireKeyDownEvent( {
  1050. keyCode: keyCodes.arrowup,
  1051. preventDefault: sinon.spy(),
  1052. stopPropagation: sinon.spy()
  1053. } );
  1054. expectChildViewsIsOnState( [ false, false, false, false, true ] );
  1055. fireKeyDownEvent( {
  1056. keyCode,
  1057. preventDefault: sinon.spy(),
  1058. stopPropagation: sinon.spy()
  1059. } );
  1060. sinon.assert.calledOnce( spy );
  1061. assertCommandOptions( spy.getCall( 0 ).args[ 0 ], '@', feedItems[ 4 ] );
  1062. const start = model.createPositionAt( doc.getRoot().getChild( 0 ), 4 );
  1063. const expectedRange = model.createRange( start, start.getShiftedBy( 1 ) );
  1064. expect( spy.getCall( 0 ).args[ 0 ].range.isEqual( expectedRange ) ).to.be.true;
  1065. } );
  1066. } );
  1067. it( 'should do nothing if panel is not visible on ' + name, () => {
  1068. setData( model, '<paragraph>foo []</paragraph>' );
  1069. model.change( writer => {
  1070. writer.insertText( '@', doc.selection.getFirstPosition() );
  1071. } );
  1072. const command = editor.commands.get( 'mention' );
  1073. const spy = testUtils.sinon.spy( command, 'execute' );
  1074. return waitForDebounce()
  1075. .then( () => {
  1076. expect( panelView.isVisible ).to.be.true;
  1077. expect( editor.model.markers.has( 'mention' ) ).to.be.true;
  1078. fireKeyDownEvent( {
  1079. keyCode: keyCodes.esc,
  1080. preventDefault: sinon.spy(),
  1081. stopPropagation: sinon.spy()
  1082. } );
  1083. expect( panelView.isVisible ).to.be.false;
  1084. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  1085. fireKeyDownEvent( {
  1086. keyCode,
  1087. preventDefault: sinon.spy(),
  1088. stopPropagation: sinon.spy()
  1089. } );
  1090. sinon.assert.notCalled( spy );
  1091. expect( panelView.isVisible ).to.be.false;
  1092. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  1093. } );
  1094. } );
  1095. }
  1096. } );
  1097. describe( 'execute', () => {
  1098. beforeEach( () => createClassicTestEditor( staticConfig ) );
  1099. it( 'should call the mention command with proper options', () => {
  1100. setData( model, '<paragraph>foo []</paragraph>' );
  1101. model.change( writer => {
  1102. writer.insertText( '@', doc.selection.getFirstPosition() );
  1103. } );
  1104. const command = editor.commands.get( 'mention' );
  1105. const spy = testUtils.sinon.spy( command, 'execute' );
  1106. return waitForDebounce()
  1107. .then( () => {
  1108. mentionsView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
  1109. sinon.assert.calledOnce( spy );
  1110. const commandOptions = spy.getCall( 0 ).args[ 0 ];
  1111. assertCommandOptions( commandOptions, '@', { id: '@Barney', text: '@Barney' } );
  1112. const start = model.createPositionAt( doc.getRoot().getChild( 0 ), 4 );
  1113. const expectedRange = model.createRange( start, start.getShiftedBy( 1 ) );
  1114. expect( commandOptions.range.isEqual( expectedRange ) ).to.be.true;
  1115. } );
  1116. } );
  1117. it( 'should hide panel on execute', () => {
  1118. setData( model, '<paragraph>foo []</paragraph>' );
  1119. model.change( writer => {
  1120. writer.insertText( '@', doc.selection.getFirstPosition() );
  1121. } );
  1122. return waitForDebounce()
  1123. .then( () => {
  1124. mentionsView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
  1125. expect( panelView.isVisible ).to.be.false;
  1126. expect( editor.model.markers.has( 'mention' ) ).to.be.false;
  1127. } );
  1128. } );
  1129. it( 'should focus view after command execution', () => {
  1130. const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
  1131. setData( model, '<paragraph>foo []</paragraph>' );
  1132. model.change( writer => {
  1133. writer.insertText( '@', doc.selection.getFirstPosition() );
  1134. } );
  1135. return waitForDebounce()
  1136. .then( () => {
  1137. mentionsView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
  1138. sinon.assert.calledOnce( focusSpy );
  1139. } );
  1140. } );
  1141. } );
  1142. function createClassicTestEditor( mentionConfig ) {
  1143. return ClassicTestEditor
  1144. .create( editorElement, {
  1145. plugins: [ Paragraph, MentionEditing, MentionUI ],
  1146. mention: mentionConfig
  1147. } )
  1148. .then( newEditor => {
  1149. editor = newEditor;
  1150. model = editor.model;
  1151. doc = model.document;
  1152. editingView = editor.editing.view;
  1153. mentionUI = editor.plugins.get( MentionUI );
  1154. panelView = mentionUI.panelView;
  1155. mentionsView = mentionUI._mentionsView;
  1156. editingView.attachDomRoot( editorElement );
  1157. // Focus the engine.
  1158. editingView.document.isFocused = true;
  1159. editingView.getDomRoot().focus();
  1160. // Remove all selection ranges from DOM before testing.
  1161. window.getSelection().removeAllRanges();
  1162. } );
  1163. }
  1164. function waitForDebounce() {
  1165. return new Promise( resolve => {
  1166. setTimeout( () => {
  1167. resolve();
  1168. }, 50 );
  1169. } );
  1170. }
  1171. function fireKeyDownEvent( options ) {
  1172. const eventInfo = new EventInfo( editingView.document, 'keydown' );
  1173. const eventData = new DomEventData( editingView.document, {
  1174. target: document.body
  1175. }, options );
  1176. editingView.document.fire( eventInfo, eventData );
  1177. }
  1178. function stubSelectionRects( rects ) {
  1179. const originalViewRangeToDom = editingView.domConverter.viewRangeToDom;
  1180. // Mock selection rect.
  1181. sinon.stub( editingView.domConverter, 'viewRangeToDom' ).callsFake( ( ...args ) => {
  1182. const domRange = originalViewRangeToDom.apply( editingView.domConverter, args );
  1183. sinon.stub( domRange, 'getClientRects' )
  1184. .returns( rects );
  1185. return domRange;
  1186. } );
  1187. }
  1188. function expectChildViewsIsOnState( expectedState ) {
  1189. const childViews = [ ...mentionsView.items ].map( item => item.children.get( 0 ) );
  1190. expect( childViews.map( child => child.isOn ) ).to.deep.equal( expectedState );
  1191. }
  1192. function assertCommandOptions( commandOptions, marker, item ) {
  1193. expect( commandOptions ).to.have.property( 'marker', marker );
  1194. expect( commandOptions ).to.have.property( 'range' );
  1195. expect( commandOptions ).to.have.property( 'mention' );
  1196. const mentionForCommand = commandOptions.mention;
  1197. for ( const key of Object.keys( item ) ) {
  1198. expect( mentionForCommand[ key ] ).to.equal( item[ key ] );
  1199. }
  1200. }
  1201. } );