EmojiAutoCompleteHelper.js 4.8 KB

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