GitHub Custom Emojis

Add custom emojis from json source

当前为 2017-05-16 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Custom Emojis
  3. // @version 0.2.6
  4. // @description Add custom emojis from json source
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/StylishThemes
  8. // @include /https?://((gist)\.)?github\.com/
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_info
  14. // @connect *
  15. // @run-at document-end
  16. // @require https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js
  17. // @require https://greasyfork.org/scripts/16936-ichord-caret-js/code/ichord-Caretjs.js?version=138639
  18. // @require https://greasyfork.org/scripts/16996-ichord-at-js-mod/code/ichord-Atjs-mod.js?version=138632
  19. // @require https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.1.2/js/ion.rangeSlider.min.js
  20. // ==/UserScript==
  21. /* global jQuery, GM_addStyle, GM_getValue, GM_setValue, GM_xmlhttpRequest, GM_info */
  22. /* eslint-disable indent, quotes */
  23. (function($) {
  24. 'use strict';
  25.  
  26. var ghe = {
  27.  
  28. version : GM_info.script.version,
  29.  
  30. vars : {
  31. // delay until package.json allowed to load
  32. delay : 8.64e7, // 24 hours in milliseconds
  33.  
  34. // base url to fetch package.json
  35. root : 'https://raw.githubusercontent.com/StylishThemes/GitHub-Custom-Emojis/master/',
  36. emojiClass : 'ghe-custom-emoji',
  37. emojiTxtTemplate : '~${name}',
  38. emojiImgTemplate : ':_${name}:',
  39. maxEmojiZoom : 3,
  40. maxEmojiHeight : 150,
  41.  
  42. // Keyboard shortcut to open panel
  43. keyboardOpen : 'g+=',
  44. keyboardDelay : 1000
  45. },
  46.  
  47. regex : {
  48. // nodes to skip while traversing the dom
  49. skipElm : /^(script|style|svg|iframe|br|meta|link|textarea|input|code|pre)$/i,
  50. // emoji template
  51. template : /\$\{name\}/,
  52. // character to escape in regex
  53. charsToEsc : /[-\/\\^$*+?.()|[\]{}]/g
  54. },
  55.  
  56. defaults : {
  57. activeZoom : 1.8,
  58. caseSensitive : false,
  59. rangeHeight : '20;40', // min;max as set by ion.rangeSlider
  60. insertAsImage : false,
  61. // emoji json sources
  62. sources : [
  63. 'https://raw.githubusercontent.com/StylishThemes/GitHub-Custom-Emojis/master/collections/emoji-custom.json',
  64. 'https://raw.githubusercontent.com/StylishThemes/GitHub-Custom-Emojis/master/collections/emoji-crazy-rabbit.json',
  65. 'https://raw.githubusercontent.com/StylishThemes/GitHub-Custom-Emojis/master/collections/emoji-onion-head.json',
  66. 'https://raw.githubusercontent.com/StylishThemes/GitHub-Custom-Emojis/master/collections/emoji-unicode.json',
  67. 'https://raw.githubusercontent.com/StylishThemes/GitHub-Custom-Emojis/master/collections/emoji-custom-text.json'
  68. ]
  69. },
  70.  
  71. // emoji json stored here
  72. collections : {},
  73.  
  74. // GitHub ajax containers
  75. containers : [
  76. '#js-pjax-container',
  77. '#js-repo-pjax-container',
  78. '.js-contribution-activity',
  79. '.more-repos', // loading "more" of "Your repositories"
  80. '#dashboard .news', // loading "more" news
  81. '.js-preview-body' // comment previews
  82. ],
  83.  
  84. // promises used when loading JSON
  85. promises : {},
  86.  
  87. getStoredValues : function() {
  88. var defaults = this.defaults;
  89. this.settings = {
  90. rangeHeight : GM_getValue('rangeHeight', defaults.rangeHeight),
  91. activeZoom : GM_getValue('activeZoom', defaults.activeZoom),
  92. caseSensitive : GM_getValue('caseSensitive', defaults.caseSensitive),
  93. insertAsImage : GM_getValue('insertAsImage', defaults.insertAsImage),
  94. sources : GM_getValue('sources', defaults.sources),
  95.  
  96. date : GM_getValue('date', 0)
  97. };
  98.  
  99. this.collections = GM_getValue('collections', {});
  100.  
  101. debug('Retrieved stored values & collections', this.settings, this.collections);
  102. },
  103.  
  104. storeVal : function(key, set, $el) {
  105. var tmp,
  106. val = set[key];
  107. GM_setValue(key, val);
  108. if (typeof val === 'boolean') {
  109. $el.prop('checked', val);
  110. } else {
  111. $el.val(val);
  112. }
  113. // update sliders
  114. if ($el.hasClass('ghe-height')) {
  115. tmp = val.split(';');
  116. $el.data('ionRangeSlider').update({
  117. from: tmp[0],
  118. to: tmp[1]
  119. });
  120. } else if ($el.hasClass('ghe-zoom')) {
  121. $el.data('ionRangeSlider').update({
  122. from: val
  123. });
  124. }
  125. },
  126.  
  127. setStoredValues : function(reset) {
  128. var $el, tmp, len, indx,
  129. s = ghe.settings,
  130. d = ghe.defaults,
  131. $panel = $('#ghe-settings-inner');
  132.  
  133. ghe.busy = true;
  134. ghe.storeVal('caseSensitive', reset ? d : s, $panel.find('.ghe-case'));
  135. ghe.storeVal('insertAsImage', reset ? d : s, $panel.find('.ghe-image'));
  136. ghe.storeVal('activeZoom', reset ? d : s, $panel.find('.ghe-zoom'));
  137. ghe.storeVal('rangeHeight', reset ? d : s, $panel.find('.ghe-height'));
  138.  
  139. GM_setValue('collections', this.collections);
  140. GM_setValue('date', s.date);
  141.  
  142. if (reset) {
  143. // add defaults back into source list; but don't remove any new stuff
  144. len = d.sources.length;
  145. for (indx = 0; indx < len; indx++) {
  146. if (s.sources.indexOf(d.sources[indx]) < 0) {
  147. s.sources[s.sources.length] = d.sources[indx];
  148. }
  149. }
  150. } else if (reset === false) {
  151. // Refresh sources, so clear out collections
  152. this.collections = {};
  153. }
  154. tmp = s.sources;
  155. len = tmp.length;
  156. GM_setValue('sources', tmp);
  157. for (indx = 0; indx < len; indx++) {
  158. if ($panel.find('.ghe-source').eq(indx).length) {
  159. $el = $panel
  160. .find('.ghe-source-input')
  161. .eq(indx)
  162. .attr('data-url', tmp[indx]);
  163. } else {
  164. $el = $(ghe.sourceHTML)
  165. .appendTo($panel.find('.ghe-sources'))
  166. .find('.ghe-source-input')
  167. .attr('data-url', tmp[indx]);
  168. }
  169. // only show file name when not focused
  170. ghe.showFileName($el);
  171. }
  172. // remove extras
  173. $panel.find('.ghe-source').filter(':gt(' + len + ')').remove();
  174. if (reset) {
  175. this.updateSettings();
  176. }
  177. if (typeof reset === 'boolean') {
  178. // reset autocomplete after refresh or restore so we're using the
  179. // most up-to-date collection data
  180. $('.comment-form-textarea').atwho('destroy');
  181. }
  182. debug((reset ? 'Resetting' : 'Saving') + ' current values & updating panel', s);
  183. ghe.busy = false;
  184. },
  185.  
  186. updateSettings : function() {
  187. this.isUpdating = true;
  188. var settings = this.settings,
  189. $panel = $('#ghe-settings-inner');
  190. settings.rangeHeight = $panel.find('.ghe-height').val();
  191. settings.activeZoom = $panel.find('.ghe-zoom').val();
  192. settings.insertAsImage = $panel.find('.ghe-image').is(':checked');
  193. settings.caseSensitive = $panel.find('.ghe-case').is(':checked');
  194. settings.sources = $panel.find('.ghe-source-input').map(function() {
  195. return $(this).attr('data-url');
  196. }).get();
  197.  
  198. // update case-sensitive regex
  199. this.setRegex();
  200.  
  201. debug('Updating user settings', settings);
  202. this.updateStyleSheet();
  203. this.isUpdating = false;
  204. },
  205.  
  206. loadEmojiJson : function(update) {
  207. // only load emoji.json once a day, or after a forced update
  208. if (update || (new Date().getTime() > this.settings.date + this.vars.delay)) {
  209. var indx,
  210. promises = [],
  211. sources = this.settings.sources,
  212. len = sources.length;
  213. for (indx = 0; indx < len; indx++) {
  214. promises[promises.length] = this.fetchCustomEmojis(sources[indx]);
  215. }
  216. $.when.apply(null, promises).done(function() {
  217. ghe.checkPage();
  218. ghe.promises = [];
  219. ghe.settings.date = new Date().getTime();
  220. GM_setValue('date', ghe.settings.date);
  221. GM_setValue('collections', ghe.collections);
  222. });
  223. }
  224. },
  225.  
  226. fetchCustomEmojis : function(url) {
  227. if (!this.promises[url]) {
  228. this.promises[url] = $.Deferred(function(defer) {
  229. debug('Fetching custom emoji list', url);
  230. GM_xmlhttpRequest({
  231. method : 'GET',
  232. url : url,
  233. onload : function(response) {
  234. var json = false;
  235. try {
  236. json = JSON.parse(response.responseText);
  237. } catch (err) {
  238. debug('Invalid JSON', url);
  239. return defer.reject();
  240. }
  241. if (json && json[0].name) {
  242. // save url to make removing the entry easier
  243. json[0].url = url;
  244. ghe.collections[json[0].name] = json;
  245. debug('Adding "' + json[0].name + '" Emoji Collection');
  246. }
  247. return defer.resolve();
  248. }
  249. });
  250. }).promise();
  251. }
  252. return this.promises[url];
  253. },
  254.  
  255. // Using: document.evaluate('//*[text()[contains(.,":_")]]', document.body, null,
  256. // XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotItem(0);
  257. // to find matching content as it is much faster than scanning each node
  258. checkPage : function() {
  259. this.isUpdating = true;
  260. var node,
  261. indx = 0,
  262. parts = this.vars.emojiImgTemplate.split('${name}'), // parts = [':_', ':']
  263. // adding "//" starts from document, so if node is defined, don't
  264. // include it so the search starts from the node
  265. path = '//*[text()[contains(.,"' + parts[0] + '")]]',
  266. nodes = document.evaluate(path, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null),
  267. len = nodes.snapshotLength;
  268. try {
  269. node = nodes.snapshotItem(indx);
  270. while (node && indx++ < len) {
  271. if (!ghe.regex.skipElm.test(node.nodeName)) {
  272. ghe.findEmoji(node);
  273. }
  274. node = nodes.snapshotItem(indx);
  275. }
  276. } catch (e) {
  277. debug('Nothing to replace!', e);
  278. }
  279. this.isUpdating = false;
  280. },
  281.  
  282. findEmoji : function(node) {
  283. var indx, len, group, match, matchesLen, name,
  284. regex = ghe.regex.nameRegex,
  285. matches = [],
  286. emojis = this.collections,
  287. str = node.textContent;
  288. while ((match = regex.exec(str)) !== null) {
  289. matches[matches.length] = match[1];
  290. }
  291. if (matches && matches[0]) {
  292. matchesLen = matches.length;
  293. for (group in emojis) {
  294. // cycle through the collections (except text type)
  295. if (emojis.hasOwnProperty(group) && emojis[group][0].type !== 'text') {
  296. len = emojis[group].length;
  297. for (indx = 1; indx < len; indx++) {
  298. name = emojis[group][indx].name;
  299. for (match = 0; match < matchesLen; match++) {
  300. if (name === matches[match]) {
  301. debug('found "' + matches[match] + '" in "' + node.textContent + '"');
  302. ghe.replaceText(node, emojis[group][indx]);
  303. }
  304. }
  305. }
  306. }
  307. }
  308. }
  309. },
  310.  
  311. replaceText : function(node, emoji) {
  312. var data, pos, imgnode, middlebit, endbit,
  313. isCased = this.settings.caseSensitive,
  314. name = this.vars.emojiImgTemplate.replace(ghe.regex.template, emoji.name),
  315. skip = 0;
  316. name = isCased ? name : name.toUpperCase();
  317. // Code modified from highlight-5 (MIT license)
  318. // http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
  319. if (node.nodeType === 3) {
  320. data = isCased ? node.data : node.data.toUpperCase();
  321. pos = data.indexOf(name);
  322. pos -= (data.substr(0, pos).length - node.data.substr(0, pos).length);
  323. if (pos >= 0) {
  324. imgnode = ghe.createEmoji(emoji);
  325. middlebit = node.splitText(pos);
  326. endbit = middlebit.splitText(name.length);
  327. middlebit.parentNode.replaceChild(imgnode, middlebit);
  328. skip = 1;
  329. }
  330. } else if (node.nodeType === 1 && node.childNodes) {
  331. for (var i = 0; i < node.childNodes.length; ++i) {
  332. i += ghe.replaceText(node.childNodes[i], emoji);
  333. }
  334. }
  335. return skip;
  336. },
  337.  
  338. // This function does the surrounding for every matched piece of text
  339. // and can be customized to do what you like
  340. // <img class="emoji" title=":smile:" alt=":smile:" src="x.png" height="20" width="20" align="absmiddle">
  341. createEmoji : function(emoji) {
  342. var el = document.createElement('img');
  343. el.src = emoji.url;
  344. el.className = ghe.vars.emojiClass + ' emoji';
  345. el.title = el.alt = ghe.vars.emojiImgTemplate.replace(ghe.regex.template, emoji.name);
  346. // el.align = 'absmiddle'; // deprecated attribute
  347. return el;
  348. },
  349.  
  350. // used by autocomplete (atwho) filter function
  351. matches : function(query, labels) {
  352. if (query === '') {
  353. return 1;
  354. }
  355. labels = labels || '';
  356. var i, partial,
  357. count = 0,
  358. isCS = this.settings.caseSensitive,
  359. arry = (isCS ? labels : labels.toUpperCase()).split(/[\s,_]+/),
  360. parts = (isCS ? query : query.toUpperCase()).split(/[,_]/),
  361. len = parts.length;
  362. for (i = 0; i < len; i++) {
  363. // full match or partial
  364. partial = arry.join('_').indexOf(parts.join('_'));
  365. if (arry.indexOf(parts[i]) > -1 || partial > -1) {
  366. count++;
  367. }
  368. // give more weight to results with indexOf closer to zero
  369. if (partial > -1 && partial < len / 2) {
  370. count++;
  371. }
  372. }
  373. // return fraction of query matches
  374. return count / len;
  375. },
  376.  
  377. emojiSort : function(a, b) {
  378. return a.name > b.name ? 1 : a.name < b.name ? -1 : 0;
  379. },
  380.  
  381. // init when comment textarea is focused
  382. initAutocomplete : function($el) {
  383. if (!$el.data('atwho')) {
  384. var indx, imgLen, txtLen, name, group,
  385. text = [],
  386. data = [];
  387. // combine data
  388. for (name in ghe.collections) {
  389. if (ghe.collections.hasOwnProperty(name)) {
  390. group = ghe.collections[name].slice(1);
  391. if (ghe.collections[name][0].type === 'text') {
  392. text = text.concat(group);
  393. } else {
  394. data = data.concat(group);
  395. }
  396. }
  397. }
  398. imgLen = data.length;
  399. if (imgLen) {
  400. // alphabetic sort
  401. data = data.sort(ghe.emojiSort);
  402. // add prepend name to labels
  403. for (indx = 0; indx < imgLen; indx++) {
  404. data[indx].labels = data[indx].name.replace(/_/g, ' ') + ' ' + data[indx].labels;
  405. }
  406. // add emoji autocomplete to comment textareas
  407. $el.atwho({
  408. // first two characters from emojiImgTemplate
  409. at : ghe.vars.emojiImgTemplate.split('${name}')[0],
  410. data : data,
  411. searchKey: 'labels',
  412. displayTpl : '<li><span><img src="${url}" height="30" /></span>${name}</li>',
  413. insertTpl : ghe.vars.emojiImgTemplate,
  414. delay : 400,
  415. callbacks : {
  416. matcher: function(flag, subtext) {
  417. var regexp = ghe.regex.emojiImgFilter,
  418. match = regexp.exec(subtext);
  419. // this next line does some magic...
  420. // for some reason, without it, moving the caret from "p" to "r" in
  421. // ":_people,fear," opens & closes the popup with each letter typed
  422. subtext.match(regexp);
  423. if (match) {
  424. return match[2] || match[1];
  425. } else {
  426. return null;
  427. }
  428. },
  429. filter: function(query, data, searchKey) {
  430. var i, item,
  431. len = data.length,
  432. _results = [];
  433. for (i = 0; i < len; i++) {
  434. item = data[i];
  435. item.atwho_order = ghe.matches(query, item[searchKey]);
  436. if (item.atwho_order > 0.9) {
  437. _results[_results.length] = item;
  438. }
  439. }
  440. return query === '' ? _results : _results.sort(function(a, b) {
  441. // descending sort
  442. return b.atwho_order - a.atwho_order;
  443. });
  444. },
  445. sorter: function(query, items) {
  446. // sorted by filter
  447. return items;
  448. },
  449. // event parameter adding in atwho.js mod
  450. beforeInsert: function(value, $li, event) {
  451. if (event.shiftKey || ghe.settings.insertAsImage) {
  452. // add image tag directly if shift is held
  453. return '<img title="' +
  454. ghe.vars.emojiImgTemplate.replace(ghe.regex.template, $li.text()) +
  455. '" src="' + $li.find('img').attr('src') + '">';
  456. }
  457. return value;
  458. }
  459. }
  460. });
  461. }
  462.  
  463. txtLen = text.length;
  464. if (txtLen) {
  465. // alphabetic sort
  466. text = text.sort(ghe.emojiSort);
  467. $el.atwho({
  468. at : ghe.vars.emojiTxtTemplate.split('${name}')[0],
  469. data : text,
  470. searchKey: 'name',
  471. // add data-emoji because of Emoji-One Chrome extension adds
  472. // hidden text and an svg image inside the span
  473. displayTpl : '<li data-emoji="${text}"><span class="ghe-text">${text}</span>${name}</li>',
  474. insertTpl : ghe.vars.emojiTxtTemplate,
  475. delay : 400,
  476. callbacks : {
  477. matcher: function(flag, subtext) {
  478. var regexp = ghe.regex.emojiTxtFilter,
  479. match = regexp.exec(subtext);
  480. // this next line does some magic...
  481. subtext.match(regexp);
  482. if (match) {
  483. return match[2] || match[1];
  484. } else {
  485. return null;
  486. }
  487. },
  488. filter: function(query, data, searchKey) {
  489. var i, item,
  490. len = data.length,
  491. _results = [];
  492. for (i = 0; i < len; i++) {
  493. item = data[i];
  494. item.atwho_order = ghe.matches(query, item[searchKey]);
  495. if (item.atwho_order > 0.9) {
  496. _results[_results.length] = item;
  497. }
  498. }
  499. return query === '' ? _results : _results.sort(function(a, b) {
  500. // descending sort
  501. return b.atwho_order - a.atwho_order;
  502. });
  503. },
  504. sorter: function(query, items) {
  505. // sorted by filter
  506. return items;
  507. },
  508. // event parameter adding in atwho.js mod
  509. beforeInsert: function(value, $li, event) {
  510. return $li.attr('data-emoji');
  511. }
  512. }
  513. });
  514. }
  515. // use classes from GitHub-Dark to make theme match GitHub-Dark
  516. $('.atwho-view').addClass('popover suggester');
  517. }
  518. },
  519.  
  520. addToolbarIcon : function() {
  521. // add Emoji setting icons
  522. var indx, $el,
  523. $toolbars = $('.toolbar-commenting'),
  524. len = $toolbars.length;
  525. for (indx = 0; indx < len; indx++) {
  526. $el = $toolbars.eq(indx);
  527. if (!$el.find('.ghe-settings-icon').length) {
  528. $el.prepend([
  529. '<button type="button" class="ghe-settings-open toolbar-item tooltipped tooltipped-n tooltipped-multiline" aria-label="Browse collections & Set Emojis Options" tabindex="-1">',
  530. '<svg class="ghe-settings-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor">',
  531. '<path d="M7.205 3.233c0 .952-.753 1.73-1.722 1.73-.953 0-1.707-.793-1.707-1.73 0-.937.762-1.73 1.707-1.73.97 0 1.73.793 1.73 1.73h-.008zm6.904 0c0 .952-.794 1.73-1.747 1.73-.95 0-1.722-.793-1.722-1.73 0-.937.795-1.73 1.73-1.73.938 0 1.747.793 1.747 1.73h-.008zM7.204 10.1v5.19c0 1.728 6.904 1.728 6.904 0V10.1M10.642 10.1v3.46"/>',
  532. '<path d="M.878 8.777s3.167 1.893 8.002 1.92c4.365.02 8.135-1.92 8.135-1.92"/>',
  533. '</svg>',
  534. '</button>'
  535. ].join(''));
  536. }
  537. }
  538. },
  539.  
  540. // dynamic stylesheet
  541. updateStyleSheet : function() {
  542. var range = this.settings.rangeHeight.split(';');
  543. ghe.$style.text([
  544. // img styling - vertically center with set height range
  545. '.atwho-view li img, #ghe-popup .select-menu-item img, img[alt="ghe-emoji"], .' +
  546. this.vars.emojiClass + ' { ' +
  547. 'margin-bottom:.25em; vertical-align:middle; ' +
  548. 'min-height: ' + (range[0] || 'none') + 'px;' +
  549. 'max-height: ' + (range[1] || 'none') + 'px }',
  550. // click (make active) on image to zoom
  551. '.' + this.vars.emojiClass + ':active, a:active img[alt="ghe-emoji"] { zoom:' +
  552. this.settings.activeZoom + ' }'
  553. ].join(''));
  554. },
  555.  
  556. addBindings : function() {
  557. var lastKey,
  558. $popup = $('#ghe-popup'),
  559. $settings = $('#ghe-settings');
  560. // Delegated bindings
  561. $('body')
  562. .on('click', '.ghe-settings-open', function() {
  563. // open all collections panel
  564. ghe.openCollections($(this));
  565. return false;
  566. })
  567. .on('click', '.ghe-collection', function() {
  568. // open targeted collection
  569. var name = $(this).attr('data-group');
  570. ghe.showCollection(name);
  571. })
  572. .on('click', '.ghe-emoji', function(e) {
  573. // click on emoji in collection to add to textarea
  574. ghe.addEmoji(e, $(this));
  575. })
  576. .on('click keypress keydown', function(e) {
  577. clearTimeout(ghe.timer);
  578. var panelVisible = $popup.hasClass('in') || $settings.hasClass('in'),
  579. openPanel = ghe.vars.keyboardOpen.split('+'),
  580. key = String.fromCharCode(e.which).toLowerCase();
  581. // press escape or click outside to close the panel
  582. if (panelVisible && e.which === 27 || e.type === 'click' && !$(e.target).closest('#ghe-wrapper').length) {
  583. ghe.closePanels();
  584. return;
  585. }
  586. // keydown is only needed for escape key detection
  587. if (e.type === 'keydown' || /(input|textarea)/i.test(document.activeElement.nodeName)) {
  588. return;
  589. }
  590. // shortcut keys need keypress
  591. if (lastKey === openPanel[0] && key === openPanel[1]) {
  592. if ($settings.hasClass('in')) {
  593. ghe.closePanels();
  594. } else {
  595. ghe.openSettings();
  596. }
  597. }
  598. lastKey = key;
  599. ghe.timer = setTimeout(function() {
  600. lastKey = null;
  601. }, ghe.vars.keyboardDelay);
  602.  
  603. // add shortcut to help menu
  604. if (key === '?') {
  605. // table doesn't exist until user presses "?"
  606. setTimeout(function() {
  607. if (!$('.ghe-shortcut').length) {
  608. $('.keyboard-mappings:eq(0) tbody:eq(0)').append([
  609. '<tr class="ghe-shortcut">',
  610. '<td class="keys">',
  611. '<kbd>' + openPanel[0] + '</kbd> <kbd>' + openPanel[1] + '</kbd>',
  612. '</td>',
  613. '<td>GitHub Emojis: open settings</td>',
  614. '</tr>'
  615. ].join(''));
  616. }
  617. }, 300);
  618. }
  619. });
  620.  
  621. // popup & settings interactions
  622. $('#ghe-popup .octicon-gear').on('click keyup', function(e) {
  623. if (e.type === 'keyup' && e.which !== 13) {
  624. return;
  625. }
  626. ghe.openSettings();
  627. });
  628. $('#ghe-settings, #ghe-settings-close, #ghe-settings-inner').on('click', function(e) {
  629. if (this.id === 'ghe-settings-inner') {
  630. e.stopPropagation();
  631. } else {
  632. ghe.closePanels();
  633. }
  634. });
  635. // ghe-checkbox added to checkboxes
  636. $('.ghe-checkbox').on('change', function() {
  637. ghe.updateSettings();
  638. });
  639. // go back - switch from single collection to showing all collections
  640. $('#ghe-popup .ghe-back').on('click', function() {
  641. $('.ghe-single-collection, .ghe-back').hide();
  642. $('.ghe-all-collections').show();
  643. });
  644.  
  645. // add new source input
  646. $('#ghe-add-source').on('click', function() {
  647. var $panel = $('#ghe-settings-inner');
  648. // lets not get crazy!
  649. if ($panel.find('.ghe-source').length < 20) {
  650. $(ghe.sourceHTML).appendTo($panel.find('.ghe-sources'));
  651. }
  652. return false;
  653. });
  654. $('#ghe-refresh-sources, #ghe-restore').on('click', function() {
  655. // update sources from settings panel
  656. ghe.setStoredValues(this.id === 'ghe-restore');
  657. // load json files
  658. ghe.loadEmojiJson(true);
  659. return false;
  660. });
  661.  
  662. // Init range slider
  663. $('.ghe-height')
  664. .val(ghe.settings.rangeHeight)
  665. .ionRangeSlider({
  666. type : 'double',
  667. min : 0,
  668. max : ghe.vars.maxEmojiHeight,
  669. onChange : function() {
  670. ghe.updateSettings();
  671. },
  672. force_edges : true,
  673. hide_min_max : true
  674. });
  675. $('.ghe-zoom')
  676. .val(ghe.settings.activeZoom)
  677. .ionRangeSlider({
  678. min : 0,
  679. max : ghe.vars.maxEmojiZoom,
  680. step : 0.1,
  681. onChange : function() {
  682. ghe.updateSettings();
  683. },
  684. force_edges : true,
  685. hide_min_max : true
  686. });
  687.  
  688. // Remove source input - delegated binding
  689. $('.ghe-settings-wrapper')
  690. .on('click', '.ghe-remove', function() {
  691. var $wrapper = $(this).closest('.ghe-source'),
  692. url = $wrapper.find('.ghe-source-input').attr('data-url');
  693. ghe.removeSource(url);
  694. $wrapper.remove();
  695. ghe.setStoredValues();
  696. return false;
  697. })
  698. .on('focus blur input change', '.ghe-source-input', function(e) {
  699. if (ghe.busy) { return; }
  700. ghe.busy = true;
  701. var val,
  702. $this = $(this);
  703. switch (e.type) {
  704. case 'focus':
  705. case 'focusin':
  706. // show entire url when focused
  707. $this.val($this.attr('data-url'));
  708. break;
  709. case 'blur':
  710. case 'focusout':
  711. ghe.showFileName($this);
  712. break;
  713. default:
  714. $this.attr('data-url', $this.val());
  715. }
  716. if (e.type === 'change' || e.which === 13) {
  717. val = $this.val();
  718. $this.attr('data-url', val);
  719. ghe.fetchCustomEmojis(val);
  720. }
  721. ghe.busy = false;
  722. });
  723.  
  724. // initialize autocomplete that add emojis, but only on focus
  725. // since every comment has a hidden textarea
  726. $('body').on('focus', '.comment-form-textarea', function() {
  727. ghe.initAutocomplete($(this));
  728. });
  729. },
  730.  
  731. showFileName : function($el) {
  732. var str = $el.attr('data-url'),
  733. v = str.substring(str.lastIndexOf('/') + 1, str.length);
  734. // show only the file name in the input when blurred
  735. // unless there is no file name
  736. $el.val(v === '' ? str : '...' + v);
  737. },
  738.  
  739. closePanels : function() {
  740. $('#ghe-popup').removeClass('in');
  741. $('#ghe-settings').removeClass('in');
  742. ghe.$currentInput = null;
  743. },
  744.  
  745. openSettings : function() {
  746. $('.modal-backdrop').click();
  747. $('#ghe-settings').addClass('in');
  748. },
  749.  
  750. openCollections : function($el) {
  751. ghe.addCollections();
  752. var pos = $el.offset();
  753. $('#ghe-settings').removeClass('in');
  754. $('#ghe-popup')
  755. .addClass('in')
  756. .css({
  757. left: pos.left + 25,
  758. top: pos.top
  759. });
  760. ghe.$currentInput = $el.closest('.previewable-comment-form').find('.comment-form-textarea');
  761. },
  762.  
  763. addCollections : function() {
  764. var indx, len, key, group, item, emoji,
  765. collections = ghe.collections,
  766. range = ghe.settings.rangeHeight.split(';'),
  767. list = [],
  768. items = [];
  769. // build collections list -
  770. for (key in collections) {
  771. if (collections.hasOwnProperty(key)) {
  772. list[list.length] = key;
  773. }
  774. }
  775. list = list.sort(function(a, b) {
  776. return a > b ? 1 : (a < b ? -1 : 0);
  777. });
  778. len = list.length;
  779. // add random image from group
  780. for (indx = 0; indx < len; indx++) {
  781. group = collections[list[indx]];
  782. // random image (skip first entry)
  783. item = Math.round(Math.random() * (group.length - 2)) + 1;
  784. emoji = group[item];
  785. items[items.length] = '<div class="select-menu-item js-navigation-item ghe-collection' +
  786. (emoji.url ? '' : ' ghe-text-collection') +
  787. '" data-group="' + list[indx] + '">' +
  788. // collection info stored in first entry
  789. group[0].name + ' <span class="ghe-right' +
  790. (emoji.url ?
  791. // images
  792. '"><img src="' + emoji.url + '" title="' +
  793. ghe.vars.emojiImgTemplate.replace(ghe.regex.template, emoji.name) + '" style="' +
  794. 'min-height:' + (range[0] || 'none') + 'px;' +
  795. 'max-height:' + (range[1] || 'none') + 'px;">' :
  796. // text
  797. ' ghe-text" title="' + emoji.name + '" style="font-size:' + group[0].previewSize +
  798. '">' + emoji.text
  799. ) + '</span></div>';
  800. }
  801. $('.ghe-single-collection, .ghe-back').hide();
  802. $('.ghe-all-collections').html(items.join('')).show();
  803. },
  804.  
  805. showCollection : function(name) {
  806. var indx, emoji,
  807. range = ghe.settings.rangeHeight.split(';'),
  808. group = ghe.collections[name].slice(1).sort(ghe.emojiSort),
  809. list = [],
  810. len = group.length;
  811. for (indx = 1; indx < len; indx++) {
  812. emoji = group[indx];
  813. list[indx - 1] = '<div class="select-menu-item js-navigation-item ghe-emoji' +
  814. (emoji.url ? '' : ' ghe-text-emoji') +
  815. '" data-name="' + emoji.name + '">' +
  816. emoji.name + '<span class="ghe-right' +
  817. (emoji.url ?
  818. // images
  819. '"><img src="' + emoji.url + '" style="' +
  820. 'min-height:' + (range[0] || 'none') + 'px;' +
  821. 'max-height:' + (range[1] || 'none') + 'px">' :
  822. // text type
  823. ' ghe-text" style="font-size:' + ghe.collections[name][0].previewSize +
  824. // data-emoji needed because Chrome emoji-one extension adds hidden
  825. // text inside the span when it replaces the text with an svg
  826. '" data-emoji="' + emoji.text + '">' + emoji.text
  827. ) + '</span></div>';
  828. }
  829. $('.ghe-all-collections').hide();
  830. $('.ghe-single-collection').html(list.join('')).show();
  831. $('.ghe-back').show();
  832. },
  833.  
  834. // add emoji from collection
  835. addEmoji : function(e, $el) {
  836. var val, emoji,
  837. $img = $el.find('img'),
  838. name = $el.attr('data-name'),
  839. caretPos = ghe.$currentInput.caret('pos');
  840. if ($img.length) {
  841. // insert into textarea
  842. if (e.shiftKey || ghe.settings.insertAsImage) {
  843. // add image tag directly if shift is held;
  844. // GitHub does NOT allow class names so we are forced to use alt
  845. emoji = '<img alt="ghe-emoji" title="' +
  846. ghe.vars.emojiImgTemplate.replace(ghe.regex.template, name) +
  847. '" src="' + $el.find('img').attr('src') + '">';
  848. } else {
  849. emoji = ghe.vars.emojiImgTemplate.replace(ghe.regex.template, name);
  850. }
  851. } else {
  852. // insert text emoji
  853. emoji = $el.find('span').attr('data-emoji');
  854. }
  855. val = ghe.$currentInput.val();
  856. ghe.$currentInput
  857. .val(val.slice(0, caretPos) + emoji + ' ' + val.slice(caretPos))
  858. .focus()
  859. .caret('pos', caretPos + emoji.length + 1);
  860. ghe.closePanels();
  861. },
  862.  
  863. removeSource : function(url) {
  864. var indx,
  865. list = [],
  866. collections = this.collections,
  867. sources = this.settings.sources,
  868. len = sources.length;
  869. // remove from source
  870. for (indx = 0; indx < len; indx++) {
  871. if (sources[indx] !== url) {
  872. list[list.length] = sources[indx];
  873. }
  874. }
  875. this.settings.sources = list;
  876. for (indx in collections) {
  877. if (collections.hasOwnProperty(indx) && collections[indx][0].url === url) {
  878. delete collections[indx];
  879. debug('Removing "' + indx + '" collection', collections);
  880. }
  881. }
  882. },
  883.  
  884. update : function() {
  885. this.isUpdating = true;
  886. this.addToolbarIcon();
  887. // checkPage clears isUpdating flag
  888. this.checkPage();
  889. },
  890.  
  891. addPanels : function() {
  892. /* https://github.com/ichord/At.js styles for autocomplete */
  893. GM_addStyle([
  894. // settings panel
  895. '#ghe-menu:hover { cursor:pointer }',
  896. '#ghe-settings { position:fixed; z-index:65535; top:0; bottom:0; left:0; right:0; opacity:0; visibility:hidden }',
  897. '#ghe-settings.in { opacity:1; visibility:visible; background:rgba(0,0,0,.5) }',
  898. '#ghe-settings-inner { position:fixed; left:50%; top:50%; transform:translate(-50%,-50%); width:25rem; box-shadow:0 .5rem 1rem #111; color:#c0c0c0 }',
  899. '#ghe-settings label { margin-left:.5rem; position:relative; top:-1px }',
  900. '#ghe-settings .ghe-remove { float:right; margin-top:2px; padding:4px; cursor:pointer }',
  901. '#ghe-settings .ghe-remove-icon { position:relative; top:3px }',
  902. '#ghe-settings-close { fill:#666; float:right; cursor:pointer }',
  903. '#ghe-settings-close:hover { fill:#ccc }',
  904. '#ghe-settings .ghe-settings-wrapper { max-height:60vh; overflow-y:auto; padding:1px 10px; margin-top:6px }',
  905. '#ghe-settings .ghe-right, #ghe-popup .ghe-right { float:right }',
  906. '#ghe-settings p { line-height:25px; }',
  907. '#ghe-settings .checkbox input { margin-top:.35em }',
  908. '#ghe-settings input[type="checkbox"] { width:16px !important; height:16px !important; border-radius:3px !important }',
  909. '#ghe-settings .boxed-group-inner { padding:0; }',
  910. '#ghe-settings .ghe-footer { padding: 10px; border-top: #555 solid 1px; }',
  911. '#ghe-settings .ghe-min-height, #ghe-settings .ghe-max-height, .ghe-zoom { width: 5em; }',
  912. '#ghe-settings .ghe-source-input { width: 90%; padding:3px; margin:3px 0; border-style:solid; border-width:1px }',
  913. '#ghe-settings .ghe-slider-wrapper { height:40px; }',
  914. '#ghe-settings .ghe-slider-wrapper label { position:relative; top:22px }',
  915. '#ghe-settings .ghe-range-slider, #ghe-settings .ghe-zoom-slider { position:relative; height:40px; width:250px; float:right }',
  916.  
  917. // show emoji collections
  918. '#ghe-popup { display:none }',
  919. '#ghe-popup .ghe-content, #ghe-popup .ghe-content > div { max-height: 200px }',
  920. '#ghe-popup .octicon-gear { margin-left:4px }',
  921. '#ghe-popup .ghe-back svg { height:20px; padding:4px 14px 4px 4px }',
  922. '#ghe-popup .select-menu-item { font-size:1.1em; font-weight:bold; line-height:40px; padding:8px }',
  923. '#ghe-popup .select-menu-item.ghe-text-emoji { line-height:inherit; position:relative; padding-right:45px }',
  924. '#ghe-popup .select-menu-item.ghe-text-emoji .ghe-text { position:absolute; right:10px; top:0 }',
  925. '#ghe-popup .select-menu-item .ghe-text, .atwho-view .ghe-text { font-size:1.6em }',
  926. '.ghe-settings-icon, #ghe-popup.in { display:inline-block; vertical-align:middle }',
  927.  
  928. // autocomplete popup in comment
  929. '.atwho-view { position:absolute; top:0; left:0; display:none; margin-top:18px; border:1px solid #ddd; border-radius:3px; box-shadow:0 0 5px rgba(0,0,0,.1); min-width:300px; max-width:none!important; max-height:225px; overflow:auto; z-index:11110!important }',
  930. '.atwho-view .cur { background:#36f; color:#fff }',
  931. '.atwho-view .cur small { color:#fff }',
  932. '.atwho-view strong { color:#36F }',
  933. '.atwho-view .cur strong { color:#fff; font:700 }',
  934. '.atwho-view ul { list-style:none; padding:0; margin:auto; max-height:200px; overflow-y:auto; }',
  935. '.atwho-view ul li { display:block; padding:5px 10px; border-bottom:1px solid #ddd; cursor:pointer }',
  936. '.atwho-view li span { display:inline-block; min-width:60px; padding-right:4px }',
  937. '.atwho-view small { font-size:smaller; color:#777; font-weight:400 }',
  938.  
  939. // rangeSlider
  940. '.irs{position:relative;display:block;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}',
  941. '.irs-line{position:relative;display:block;overflow:hidden;outline:none !important}.irs-line-left,.irs-line-mid,.irs-line-right{position:absolute;display:block;top:0}',
  942. '.irs-line-left{left:0;width:9%}.irs-line-mid{left:9%;width:82%}.irs-line-right{right:0;width:9%}.irs-bar{position:absolute;display:block;left:0;width:0}.irs-bar-edge{position:absolute;display:block;top:0;left:0}',
  943. '.irs-shadow{position:absolute;display:none;left:0;width:0}.irs-slider{position:absolute;display:block;cursor:default;z-index:1}.irs-slider.type_last{z-index:2}.irs-min{position:absolute;display:block;left:0;cursor:default}',
  944. '.irs-max{position:absolute;display:block;right:0;cursor:default}.irs-from,.irs-to,.irs-single{position:absolute;display:block;top:0;left:0;cursor:default;white-space:nowrap}.irs-grid{position:absolute;display:none;bottom:0;left:0;width:100%;height:20px}',
  945. '.irs-with-grid .irs-grid{display:block}.irs-grid-pol{position:absolute;top:0;left:0;width:1px;height:8px;background:#000}.irs-grid-pol.small{height:4px}.irs-grid-text{position:absolute;bottom:0;left:0;white-space:nowrap;text-align:center;font-size:9px;line-height:9px;padding:0 3px;color:#000}',
  946. '.irs-disable-mask{position:absolute;display:block;top:0;left:-1%;width:102%;height:100%;cursor:default;background:rgba(0,0,0,0.0);z-index:2}.lt-ie9 .irs-disable-mask{background:#000;filter:alpha(opacity=0);cursor:not-allowed}.irs-disabled{opacity:0.4}',
  947. '.irs-hidden-input{position:absolute !important;display:block !important;top:0 !important;left:0 !important;width:0 !important;height:0 !important;font-size:0 !important;line-height:0 !important;padding:0 !important;margin:0 !important;outline:none !important;z-index:-9999 !important;background:none !important;border-style:solid !important;border-color:transparent !important}',
  948. '.irs-line-mid,.irs-line-left,.irs-line-right,.irs-bar,.irs-bar-edge,.irs-slider{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQQAAAC0BAMAAACAm0/4AAAAHlBMVEUAAADh5OlIPakuJnU8MZzh5Onh5Onlt8BIPamDg6ND+SBkAAAACnRSTlMAgMzMzHlXE4oe0nCEQQAAAMJJREFUeNrt1qENAkEQhtENkBDkCtCECiiBFjCgEXgMDVwJVEzGQ7KnZnJ5r4JP7E7+1tNJkCBBgoSqCQAAjNg+e6rbq717snt79GSHdu3J9hVGfE8nIVR4jgU+ZYHTVOBAAwAw4pROggQJEiRUTQAAYMRuSl9Rn/whN+UnFJizEiQUSijwKQucpgIHGgCAQatjy7a5tJkkBAlBQpAQJAQJQUJYYkKByQIA/6zPbSYJQUKQECQECUFCkBAkhCUmAMAvX+TSxQIIIKq9AAAAAElFTkSuQmCC") repeat-x}',
  949. '.irs{height:40px}.irs-with-grid{height:60px}.irs-line{height:12px;top:25px}.irs-line-left{height:12px;background-position:0 -30px}',
  950. '.irs-line-mid{height:12px;background-position:0 0}.irs-line-right{height:12px;background-position:100% -30px}.irs-bar{height:12px;top:25px;background-position:0 -60px}',
  951. '.irs-bar-edge{top:25px;height:12px;width:9px;background-position:0 -90px}.irs-shadow{height:3px;top:34px;background:#000;opacity:.25}',
  952. '.lt-ie9 .irs-shadow{filter:alpha(opacity=25)}.irs-slider{width:16px;height:18px;top:22px;background-position:0 -120px}',
  953. '.irs-slider.state_hover,.irs-slider:hover{background-position:0 -150px}.irs-min,.irs-max{color:#fff;font-size:10px;line-height:1.333;text-shadow:none;top:0;padding:1px 3px;background:#7D7E81;-moz-border-radius:4px;border-radius:4px}',
  954. '.irs-from,.irs-to,.irs-single{color:#fff;font-size:10px;line-height:1.333;text-shadow:none;padding:1px 5px;background:#534AA1;-moz-border-radius:4px;border-radius:4px}',
  955. '.irs-from:after,.irs-to:after,.irs-single:after{position:absolute;display:block;content:"";bottom:-6px;left:50%;width:0;height:0;margin-left:-3px;overflow:hidden;border:3px solid transparent;border-top-color:#534AA1}',
  956. '.irs-grid-pol{background:#e1e4e9}.irs-grid-text{color:#999}'
  957. ].join(''));
  958.  
  959. // Settings panel markup
  960. $('body').append([
  961. '<div id="ghe-wrapper">',
  962. '<div id="ghe-popup" class="select-menu-modal-holder js-menu-content js-navigation-container js-active-navigation-container">',
  963. '<div class="select-menu-modal">',
  964. '<div class="select-menu-header">',
  965. '<span class="select-menu-title">',
  966. '<text>Emoji Collections</text>',
  967. '<span class="octicon tooltipped tooltipped-w" aria-label="Change GitHub Custom Emoji Settings">',
  968. '<svg class="octicon-gear" viewBox="0 0 16 14" style="height: 16px; width: 14px;"><path d="M14 8.77V7.17l-1.94-0.64-0.45-1.09 0.88-1.84-1.13-1.13-1.81 0.91-1.09-0.45-0.69-1.92H6.17l-0.63 1.94-1.11 0.45-1.84-0.88-1.13 1.13 0.91 1.81-0.45 1.09L0 7.23v1.59l1.94 0.64 0.45 1.09-0.88 1.84 1.13 1.13 1.81-0.91 1.09 0.45 0.69 1.92h1.59l0.63-1.94 1.11-0.45 1.84 0.88 1.13-1.13-0.92-1.81 0.47-1.09 1.92-0.69zM7 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>',
  969. '</span>',
  970. '<span class="octicon tooltipped tooltipped-w ghe-back" aria-label="Go back to see all collections">',
  971. '<svg xmlns="http://www.w3.org/2000/svg" width="6.5" height="10" viewBox="0 0 6.5 10"><path d="M5.008 0l1.497 1.504-3.76 3.49 3.743 3.51L4.984 10l-4.99-5.013L5.01 0z"/></svg>',
  972. '</span>',
  973. '</span>',
  974. '</div>',
  975. '<div class="js-select-menu-deferred-content ghe-content">',
  976. '<div class="select-menu-list ghe-all-collections"></div>',
  977. '<div class="select-menu-list ghe-single-collection"></div>',
  978. '</div>',
  979. '</div>',
  980. '</div>',
  981. '<div id="ghe-settings">',
  982. '<div id="ghe-settings-inner" class="boxed-group">',
  983. '<h3>GitHub Custom Emoji Settings',
  984. '<svg id="ghe-settings-close" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="160 160 608 608"><path d="M686.2 286.8L507.7 465.3l178.5 178.5-45 45-178.5-178.5-178.5 178.5-45-45 178.5-178.5-178.5-178.5 45-45 178.5 178.5 178.5-178.5z"/></svg>',
  985. '</h3>',
  986. '<div class="boxed-group-inner">',
  987. '<form>',
  988. '<div class="ghe-settings-wrapper">',
  989. '<p>',
  990. '<label>Insert as Image:',
  991. '<sup class="tooltipped tooltipped-e" aria-label="Or Shift + select the emoji">?</sup>',
  992. '<input class="ghe-image ghe-checkbox ghe-right" type="checkbox">',
  993. '</label>',
  994. '</p>',
  995. '<p class="checkbox">',
  996. '<label>Case Sensitive <input class="ghe-case ghe-checkbox ghe-right" type="checkbox"></label>',
  997. '</p>',
  998. '<div class="ghe-slider-wrapper">',
  999. '<div class="ghe-range-slider">',
  1000. '<input type="text" class="ghe-height" value="" />',
  1001. '</div>',
  1002. '<label>Emoji Height',
  1003. '<sup class="tooltipped tooltipped-e" aria-label="Set emoji minimum & maximum&#10;height in pixels">?</sup>',
  1004. '</label>',
  1005. '</div>',
  1006. '<div class="ghe-slider-wrapper">',
  1007. '<div class="ghe-zoom-slider">',
  1008. '<input class="ghe-zoom ghe-right" type="text">',
  1009. '</div>',
  1010. '<label>Emoji Zoom',
  1011. '<sup class="tooltipped tooltipped-e" aria-label="Set Emoji zoom factor&#10;while actively clicked">?</sup>',
  1012. '</label>',
  1013. '</div>',
  1014. '<p>',
  1015. '<hr>',
  1016. '<h3>Sources',
  1017. '<a href="https://github.com/StylishThemes/GitHub-Custom-Emojis/wiki/Add-Emojis" class="tooltipped tooltipped-e tooltipped-multiline" aria-label="Click to get more details on how to set up an Emoji source JSON file">',
  1018. '<sup>?</sup>',
  1019. '</a>',
  1020. '</h3>',
  1021. '<div class="ghe-sources"></div>',
  1022. '</p>',
  1023. '</div>',
  1024. '<div class="ghe-footer">',
  1025. '<div class="btn-group">',
  1026. '<a href="#" id="ghe-add-source" class="btn btn-sm">Add Source</a>',
  1027. '<a href="#" id="ghe-refresh-sources" class="btn btn-sm">Refresh Sources</a>&nbsp;',
  1028. '</div>',
  1029. '<a href="#" id="ghe-restore" class="btn btn-sm btn-danger tooltipped tooltipped-n ghe-right" aria-label="Default sources are restored;&#10;other source will remain">Restore Defaults</a>',
  1030. '</div>',
  1031. '</form>',
  1032. '</div>',
  1033. '</div>',
  1034. '</div>',
  1035. '</div>'
  1036. ].join(''));
  1037. },
  1038.  
  1039. // JSON source inputs
  1040. sourceHTML : [
  1041. '<div class="ghe-source">',
  1042. '<input class="ghe-source-input" type="text" value="" placeholder="Add JSON sources only">',
  1043. '<a href="#" class="ghe-remove btn btn-sm btn-danger">',
  1044. '<svg class="ghe-remove-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="160 160 608 608" fill="currentColor"><path d="M686.2 286.8L507.7 465.3l178.5 178.5-45 45-178.5-178.5-178.5 178.5-45-45 178.5-178.5-178.5-178.5 45-45 178.5 178.5 178.5-178.5z"/></svg>',
  1045. '</a>',
  1046. '</div>'
  1047. ].join(''),
  1048.  
  1049. setRegex : function() {
  1050. var isCS = this.settings.caseSensitive,
  1051. // parts = [':_', ':']
  1052. imgParts = this.vars.emojiImgTemplate.split('${name}'),
  1053. txtParts = this.vars.emojiTxtTemplate.split('${name}');
  1054.  
  1055. // filter = /:_([a-zA-Z\u00c0-\u00ff0-9_,'.+-]*)$|:_([^\x00-\xff]*)$/gi
  1056. // used by atwho.js autocomplete
  1057. this.regex.emojiImgFilter = new RegExp(
  1058. imgParts[0] + '([a-zA-Z\u00c0-\u00ff0-9_,\'\.\+\-]*)$|' +
  1059. imgParts[0] + '([^\\x00-\\xff]*)$',
  1060. (isCS ? 'g' : 'gi')
  1061. );
  1062.  
  1063. this.regex.emojiTxtFilter = new RegExp(
  1064. txtParts[0] + '([a-zA-Z\u00c0-\u00ff0-9_,\'\.\+\-]*)$|' +
  1065. txtParts[0] + '([^\\x00-\\xff]*)$',
  1066. (isCS ? 'g' : 'gi')
  1067. );
  1068.  
  1069. // used by search & replace
  1070. this.regex.nameRegex = new RegExp(
  1071. imgParts[0] + '([\\w_]+)' + imgParts[1],
  1072. (isCS ? 'g' : 'gi')
  1073. );
  1074. },
  1075.  
  1076. init : function() {
  1077. debug('GitHub-Emoji Script initializing!');
  1078.  
  1079. // add style tag to head
  1080. this.$style = $('<style class="ghe-style">').appendTo('head');
  1081.  
  1082. this.getStoredValues();
  1083. this.loadEmojiJson();
  1084. this.updateStyleSheet();
  1085. this.isUpdating = true;
  1086. // regex based on case sensitive setting
  1087. this.setRegex();
  1088.  
  1089. var targets = document.querySelectorAll(this.containers.join(','));
  1090. Array.prototype.forEach.call(targets, function(target) {
  1091. new MutationObserver(function(mutations) {
  1092. mutations.forEach(function(mutation) {
  1093. // preform checks before adding code wrap to minimize function calls
  1094. if (mutation.target === target && !$.isEmptyObject(ghe.collections) &&
  1095. !(ghe.isUpdating || target.querySelector('.ghe-processed'))) {
  1096. ghe.update();
  1097. }
  1098. });
  1099. }).observe(target, {
  1100. childList : true,
  1101. subtree : true
  1102. });
  1103. });
  1104.  
  1105. this.addPanels();
  1106.  
  1107. // Add emoji autocomplete & watch for preview rendering
  1108. this.addToolbarIcon();
  1109. this.addBindings();
  1110. // update panel values after bindings (rangeslider)
  1111. this.setStoredValues();
  1112.  
  1113. // checkPage clears isUpdating flag
  1114. this.checkPage();
  1115. }
  1116. };
  1117.  
  1118. // add style at document-start
  1119. ghe.init();
  1120.  
  1121. // include a "?debug" anywhere in the browser URL to enable debugging
  1122. function debug() {
  1123. if (/\?debug/.test(window.location.href)) {
  1124. console.log.apply(console, arguments);
  1125. }
  1126. }
  1127. })(jQuery.noConflict(true));