EmojiAutoCompleteHelper.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. class EmojiAutoCompleteHelper {
  2. constructor(emojiStrategy) {
  3. this.emojiStrategy = emojiStrategy;
  4. this.emojiShortnameImageMap = {};
  5. this.initEmojiImageMap = this.initEmojiImageMap.bind(this);
  6. this.showHint = this.showHint.bind(this);
  7. this.initEmojiImageMap();
  8. }
  9. initEmojiImageMap() {
  10. for (let unicode in this.emojiStrategy) {
  11. const data = this.emojiStrategy[unicode];
  12. const shortname = data.shortname;
  13. // add image tag
  14. this.emojiShortnameImageMap[shortname] = emojione.shortnameToImage(shortname);
  15. }
  16. }
  17. /**
  18. * try to find emoji terms and show hint
  19. * @param {any} editor An editor instance of CodeMirror
  20. */
  21. showHint(editor) {
  22. // see https://regex101.com/r/gy3i03/1
  23. const pattern = /:[^:\s]+/;
  24. const currentPos = editor.getCursor();
  25. // find previous ':shortname'
  26. const sc = editor.getSearchCursor(pattern, currentPos, { multiline: false });
  27. if (sc.findPrevious()) {
  28. const isInputtingEmoji = (currentPos.line === sc.to().line && currentPos.ch === sc.to().ch);
  29. // return if it isn't inputting emoji
  30. if (!isInputtingEmoji) {
  31. return;
  32. }
  33. }
  34. else {
  35. return;
  36. }
  37. // see https://codemirror.net/doc/manual.html#addon_show-hint
  38. editor.showHint({
  39. completeSingle: false,
  40. // closeOnUnfocus: false, // for debug
  41. hint: () => {
  42. const matched = editor.getDoc().getRange(sc.from(), sc.to());
  43. const term = matched.replace(':', ''); // remove ':' in the head
  44. // get a list of shortnames
  45. const shortnames = this.searchEmojiShortnames(term);
  46. if (shortnames.length >= 1) {
  47. return {
  48. list: this.generateEmojiRenderer(shortnames),
  49. from: sc.from(),
  50. to: sc.to(),
  51. };
  52. }
  53. },
  54. });
  55. }
  56. /**
  57. * see https://codemirror.net/doc/manual.html#addon_show-hint
  58. * @param {string[]} emojiShortnames a list of shortname
  59. */
  60. generateEmojiRenderer(emojiShortnames) {
  61. return emojiShortnames.map((shortname) => {
  62. return {
  63. text: shortname,
  64. className: 'crowi-emoji-autocomplete',
  65. render: (element) => {
  66. element.innerHTML =
  67. `<div class="img-container">${this.emojiShortnameImageMap[shortname]}</div>` +
  68. `<span class="shortname-container">${shortname}</span>`;
  69. }
  70. };
  71. });
  72. }
  73. /**
  74. * transplanted from https://github.com/emojione/emojione/blob/master/examples/OTHER.md
  75. * @param {string} term
  76. * @returns {string[]} a list of shortname
  77. */
  78. searchEmojiShortnames(term) {
  79. const maxLength = 12;
  80. let results1 = [], results2 = [], results3 = [], results4 = [];
  81. const countLen1 = () => { results1.length };
  82. const countLen2 = () => { countLen1() + results2.length };
  83. const countLen3 = () => { countLen2() + results3.length };
  84. const countLen4 = () => { countLen3() + results4.length };
  85. // TODO performance tune
  86. // when total length of all results is less than `maxLength`
  87. for (let unicode in this.emojiStrategy) {
  88. const data = this.emojiStrategy[unicode];
  89. if (maxLength <= countLen1()) { break }
  90. // prefix match to shortname
  91. else if (data.shortname.indexOf(`:${term}`) > -1) {
  92. results1.push(data.shortname);
  93. continue;
  94. }
  95. else if (maxLength <= countLen2()) { continue }
  96. // partial match to shortname
  97. else if (data.shortname.indexOf(term) > -1) {
  98. results2.push(data.shortname);
  99. continue;
  100. }
  101. else if (maxLength <= countLen3()) { continue }
  102. // partial match to elements of aliases
  103. else if ((data.aliases != null) && data.aliases.find(elem => elem.indexOf(term) > -1)) {
  104. results3.push(data.shortname);
  105. continue;
  106. }
  107. else if (maxLength <= countLen4()) { continue }
  108. // partial match to elements of keywords
  109. else if ((data.keywords != null) && data.keywords.find(elem => elem.indexOf(term) > -1)) {
  110. results4.push(data.shortname);
  111. }
  112. }
  113. let results = results1.concat(results2).concat(results3).concat(results4);
  114. results = results.slice(0, maxLength);
  115. return results;
  116. }
  117. }
  118. export default EmojiAutoCompleteHelper;