EmojiAutoCompleteHelper.js 4.6 KB

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