summernote-ext-hint.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. (function (factory) {
  2. /* global define */
  3. if (typeof define === 'function' && define.amd) {
  4. // AMD. Register as an anonymous module.
  5. define(['jquery'], factory);
  6. } else {
  7. // Browser globals: jQuery
  8. factory(window.jQuery);
  9. }
  10. }(function ($) {
  11. var range = $.summernote.core.range;
  12. var list = $.summernote.core.list;
  13. var KEY = {
  14. UP: 38,
  15. DOWN: 40,
  16. ENTER: 13
  17. };
  18. var DROPDOWN_KEYCODES = [38, 40, 13];
  19. /**
  20. * @class plugin.hint
  21. *
  22. * Hello Plugin
  23. */
  24. $.summernote.addPlugin({
  25. /** @property {String} name name of plugin */
  26. name: 'hint',
  27. /**
  28. * @param {jQuery} $node
  29. */
  30. scrollTo: function ($node) {
  31. var $parent = $node.parent();
  32. $parent[0].scrollTop = $node[0].offsetTop - ($parent.innerHeight() / 2);
  33. },
  34. /**
  35. * @param {jQuery} $popover
  36. */
  37. moveDown: function ($popover) {
  38. var index = $popover.find('.active').index();
  39. this.activate($popover, (index === -1) ? 0 : (index + 1) % $popover.children().length);
  40. },
  41. /**
  42. * @param {jQuery} $popover
  43. */
  44. moveUp: function ($popover) {
  45. var index = $popover.find('.active').index();
  46. this.activate($popover, (index === -1) ? 0 : (index - 1) % $popover.children().length);
  47. },
  48. /**
  49. * @param {jQuery} $popover
  50. * @param {Number} i
  51. */
  52. activate: function ($popover, idx) {
  53. idx = idx || 0;
  54. if (idx < 0) {
  55. idx = $popover.children().length - 1;
  56. }
  57. $popover.children().removeClass('active');
  58. var $activeItem = $popover.children().eq(idx);
  59. $activeItem.addClass('active');
  60. this.scrollTo($activeItem);
  61. },
  62. /**
  63. * @param {jQuery} $popover
  64. */
  65. replace: function ($popover) {
  66. var wordRange = $popover.data('wordRange');
  67. var $activeItem = $popover.find('.active');
  68. var content = this.content($activeItem.html(), $activeItem.data('keyword'));
  69. if (typeof content === 'string') {
  70. content = document.createTextNode(content);
  71. }
  72. $popover.removeData('wordRange');
  73. wordRange.insertNode(content);
  74. range.createFromNode(content).collapse().select();
  75. },
  76. /**
  77. * @param {String} keyword
  78. * @return {Object|null}
  79. */
  80. searchKeyword: function (keyword) {
  81. var triggerChar = keyword.charAt(0);
  82. if (triggerChar === ':' && keyword.length > 1) {
  83. var trigger = keyword.toLowerCase().replace(':', '');
  84. return {
  85. type: 'emoji',
  86. list: $.grep(this.emojiKeys, function (item) {
  87. return item.indexOf(trigger) === 0;
  88. })
  89. };
  90. }
  91. return null;
  92. },
  93. /**
  94. * create items
  95. *
  96. * @param {Object} searchResult
  97. * @param {String} searchResult.type
  98. * @param {String[]} searchResult.list
  99. * @return {jQuery[]}
  100. */
  101. createItems: function (searchResult) {
  102. var items = [];
  103. var list = searchResult.list;
  104. for (var i = 0, len = list.length; i < len; i++) {
  105. var $item = $('<a class="list-group-item"></a>');
  106. $item.append(this.createItem(list[i]));
  107. $item.data('keyword', list[i]);
  108. items.push($item);
  109. }
  110. if (items.length) {
  111. items[0].addClass('active');
  112. }
  113. return items;
  114. },
  115. /**
  116. * create list item template
  117. *
  118. * @param {Object} item
  119. * @returns {String}
  120. */
  121. createItem: function (item) {
  122. var content = this.emojiInfo[item];
  123. return '<img src="' + content + '" width="20" /> :' + item + ':';
  124. },
  125. /**
  126. * create inserted content to add in summernote
  127. *
  128. * @param {String} html
  129. * @param {String} keyword
  130. * @return {Node|String}
  131. */
  132. content: function (html, item) {
  133. var url = this.emojiInfo[item];
  134. if (url) {
  135. var $img = $('<img />').attr('src', url).css({
  136. width : 20
  137. });
  138. return $img[0];
  139. }
  140. return html;
  141. },
  142. /**
  143. * @return {Promise}
  144. */
  145. loadEmojis: function () {
  146. var self = this;
  147. return $.getJSON('https://api.github.com/emojis').then(function (data) {
  148. self.emojiKeys = Object.keys(data);
  149. self.emojiInfo = data;
  150. });
  151. },
  152. init: function (layoutInfo) {
  153. var self = this;
  154. var $note = layoutInfo.holder();
  155. var $popover = $('<div class="list-group" />').css({
  156. position: 'absolute',
  157. 'max-height': 300,
  158. 'overflow-y': 'scroll',
  159. 'display': 'none'
  160. });
  161. // FIXME We need a handler for unload resources.
  162. $popover.on('click', '.list-group-item', function () {
  163. self.replace($popover);
  164. $popover.hide();
  165. $note.summernote('focus');
  166. });
  167. $(document).on('click', function () {
  168. $popover.hide();
  169. });
  170. $note.on('summernote.keydown', function (customEvent, nativeEvent) {
  171. if ($popover.css('display') !== 'block') {
  172. return;
  173. }
  174. if (nativeEvent.keyCode === KEY.DOWN) {
  175. nativeEvent.preventDefault();
  176. self.moveDown($popover);
  177. } else if (nativeEvent.keyCode === KEY.UP) {
  178. nativeEvent.preventDefault();
  179. self.moveUp($popover);
  180. } else if (nativeEvent.keyCode === KEY.ENTER) {
  181. nativeEvent.preventDefault();
  182. self.replace($popover);
  183. $popover.hide();
  184. $note.summernote('focus');
  185. }
  186. });
  187. $note.on('summernote.keyup', function (customEvent, nativeEvent) {
  188. if (DROPDOWN_KEYCODES.indexOf(nativeEvent.keyCode) === -1) {
  189. var wordRange = $(this).summernote('createRange').getWordRange();
  190. var result = self.searchKeyword(wordRange.toString());
  191. if (!result || !result.list.length) {
  192. $popover.hide();
  193. return;
  194. }
  195. layoutInfo.popover().append($popover);
  196. var rect = list.last(wordRange.getClientRects());
  197. $popover.html(self.createItems(result)).css({
  198. left: rect.left,
  199. top: rect.top + rect.height
  200. }).data('wordRange', wordRange).show();
  201. }
  202. });
  203. this.loadEmojis();
  204. },
  205. // FIXME Summernote doesn't support event pipeline yet.
  206. // - Plugin -> Base Code
  207. events: {
  208. ENTER: function () {
  209. // prevent ENTER key
  210. return false;
  211. }
  212. }
  213. });
  214. }));