mention-customization.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals ClassicEditor, console, window, document, setTimeout */
  6. import { CS_CONFIG } from '@ckeditor/ckeditor5-cloud-services/tests/_utils/cloud-services-config';
  7. ClassicEditor
  8. .create( document.querySelector( '#snippet-mention-customization' ), {
  9. cloudServices: CS_CONFIG,
  10. extraPlugins: [ CustomMention ],
  11. toolbar: {
  12. items: [
  13. 'heading', '|', 'bold', 'italic', '|', 'undo', 'redo'
  14. ],
  15. viewportTopOffset: window.getViewportTopOffsetConfig(),
  16. },
  17. mention: [
  18. {
  19. marker: '@',
  20. feed: getFeedItems,
  21. itemRenderer: customItemRenderer,
  22. minimumCharacters: 1
  23. }
  24. ]
  25. } )
  26. .then( editor => {
  27. window.editor = editor;
  28. } )
  29. .catch( err => {
  30. console.error( err.stack );
  31. } );
  32. function CustomMention( editor ) {
  33. // The upcast converter will convert <a class="mention"> elements to the model 'mention' attribute.
  34. editor.conversion.for( 'upcast' ).elementToAttribute( {
  35. view: {
  36. name: 'a',
  37. key: 'data-mention',
  38. classes: 'mention',
  39. attributes: {
  40. href: true,
  41. 'data-user-id': true
  42. }
  43. },
  44. model: {
  45. key: 'mention',
  46. value: viewItem => {
  47. // Optionally: do not convert partial mentions.
  48. if ( !isFullMention( viewItem ) ) {
  49. return;
  50. }
  51. // The mention feature expects that mention attribute value in the model is a plain object:
  52. const mentionValue = {
  53. // The name attribute is required by mention editing.
  54. name: viewItem.getAttribute( 'data-mention' ),
  55. // Add any other properties as required.
  56. link: viewItem.getAttribute( 'href' ),
  57. id: viewItem.getAttribute( 'data-user-id' )
  58. };
  59. return mentionValue;
  60. }
  61. },
  62. converterPriority: 'high'
  63. } );
  64. function isFullMention( viewElement ) {
  65. const textNode = viewElement.getChild( 0 );
  66. const dataMention = viewElement.getAttribute( 'data-mention' );
  67. // Do not parse empty mentions.
  68. if ( !textNode || !textNode.is( 'text' ) ) {
  69. return false;
  70. }
  71. const mentionString = textNode.data;
  72. // Assume that mention is set as marker + mention name.
  73. const name = mentionString.slice( 1 );
  74. // Do not upcast partial mentions - might come from copy-paste of partially selected mention.
  75. return name == dataMention;
  76. }
  77. // Don't forget to define a downcast converter as well:
  78. editor.conversion.for( 'downcast' ).attributeToElement( {
  79. model: 'mention',
  80. view: ( modelAttributeValue, viewWriter ) => {
  81. if ( !modelAttributeValue ) {
  82. // Do not convert empty attributes.
  83. return;
  84. }
  85. return viewWriter.createAttributeElement( 'a', {
  86. class: 'mention',
  87. 'data-mention': modelAttributeValue.name,
  88. 'data-user-id': modelAttributeValue.id,
  89. 'href': modelAttributeValue.link
  90. } );
  91. },
  92. converterPriority: 'high'
  93. } );
  94. }
  95. const items = [
  96. { id: '1', name: 'Barney Stinson', username: 'swarley', link: 'https://www.imdb.com/title/tt0460649/characters/nm0000439' },
  97. { id: '2', name: 'Lily Aldrin', username: 'lilypad', link: 'https://www.imdb.com/title/tt0460649/characters/nm0004989' },
  98. { id: '3', name: 'Marshall Eriksen', username: 'marshmallow', link: 'https://www.imdb.com/title/tt0460649/characters/nm0781981' },
  99. { id: '4', name: 'Robin Scherbatsky', username: 'rsparkles', link: 'https://www.imdb.com/title/tt0460649/characters/nm1130627' },
  100. { id: '5', name: 'Ted Mosby', username: 'tdog', link: 'https://www.imdb.com/title/tt0460649/characters/nm1102140' }
  101. ];
  102. function getFeedItems( feedText ) {
  103. // As an example of asynchronous action return a promise that resolves after a 100ms timeout.
  104. return new Promise( resolve => {
  105. setTimeout( () => {
  106. resolve( items.filter( isItemMatching ) );
  107. }, 100 );
  108. } );
  109. // Filtering function - it uses `name` and `username` properties of an item to find a match.
  110. function isItemMatching( item ) {
  111. // Make search case-insensitive.
  112. const searchString = feedText.toLowerCase();
  113. // Include an item in the search results if name or username includes the current user input.
  114. return textIncludesSearchSting( item.name, searchString ) || textIncludesSearchSting( item.username, searchString );
  115. }
  116. function textIncludesSearchSting( text, searchString ) {
  117. return text.toLowerCase().includes( searchString );
  118. }
  119. }
  120. function customItemRenderer( item ) {
  121. const span = document.createElement( 'span' );
  122. span.classList.add( 'custom-item' );
  123. span.id = `mention-list-item-id-${ item.id }`;
  124. span.innerHTML = `${ item.name } <span class="custom-item-username">@${ item.username }</span>`;
  125. return span;
  126. }