summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Eastwood <contact@ericeastwood.com>2017-05-03 08:18:14 -0500
committerEric Eastwood <contact@ericeastwood.com>2017-05-04 00:41:11 -0500
commitfcadee93f20b559b542e55f4a064a512266e3083 (patch)
treee4b54ce047007177784ebf413c45c85c850ee341
parent920d55b9f8afd35e16351fb57d671acf66092e89 (diff)
downloadgitlab-ce-modulify-and-tech-debt-gfm-auto-complete.tar.gz
Make `gfm_auto_complete` a module and fix up tech debtmodulify-and-tech-debt-gfm-auto-complete
Spawned from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1739
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js504
-rw-r--r--app/assets/javascripts/gl_form.js4
-rw-r--r--app/assets/javascripts/issuable_form.js4
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/notes.js1
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml3
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--spec/features/projects/gfm_autocomplete_load_spec.rb2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js6
10 files changed, 289 insertions, 241 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 0bdce52cc89..84222861710 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -50,6 +50,7 @@ import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import BlobViewer from './blob/viewer/index';
+import GfmAutoComplete from './gfm_auto_complete';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -76,6 +77,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path = page.split(':');
shortcut_handler = null;
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
+
function initBlob() {
new LineHighlighter();
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 687a462a0d4..394ae9bf6a5 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -3,109 +3,24 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
-import glRegexp from '~/lib/utils/regexp';
-
-// Creates the variables for setting up GFM auto-completion
-window.gl = window.gl || {};
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
}
-window.gl.GfmAutoComplete = {
- dataSources: {},
- defaultLoadingData: ['loading'],
- cachedData: {},
- isLoadingData: {},
- atTypeMap: {
- ':': 'emojis',
- '@': 'members',
- '#': 'issues',
- '!': 'mergeRequests',
- '~': 'labels',
- '%': 'milestones',
- '/': 'commands'
- },
- // Emoji
- Emoji: {
- templateFunction: function(name) {
- return `<li>
- ${name} ${glEmojiTag(name)}
- </li>
- `;
- }
- },
- // Team Members
- Members: {
- template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
- },
- Labels: {
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
- },
- // Issues and MergeRequests
- Issues: {
- template: '<li><small>${id}</small> ${title}</li>'
- },
- // Milestones
- Milestones: {
- template: '<li>${title}</li>'
- },
- Loading: {
- template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>'
- },
- DefaultOptions: {
- sorter: function(query, items, searchKey) {
- this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
- if (gl.GfmAutoComplete.isLoading(items)) {
- this.setting.highlightFirst = false;
- return items;
- }
- return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
- },
- filter: function(query, data, searchKey) {
- if (gl.GfmAutoComplete.isLoading(data)) {
- gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
- return data;
- } else {
- return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
- }
- },
- beforeInsert: function(value) {
- if (value && !this.setting.skipSpecialCharacterTest) {
- var withoutAt = value.substring(1);
- if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
- }
- return value;
- },
- matcher: function (flag, subtext) {
- // The below is taken from At.js source
- // Tweaked to commands to start without a space only if char before is a non-word character
- // https://github.com/ichord/At.js
- var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
- atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
- atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
- subtext = subtext.split(/\s+/g).pop();
- flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-
- _a = decodeURI("%C3%80");
- _y = decodeURI("%C3%BF");
-
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
-
- match = regexp.exec(subtext);
+class GfmAutoComplete {
+ constructor(dataSources) {
+ this.dataSources = dataSources || {};
+ this.cachedData = {};
+ this.isLoadingData = {};
+ }
- if (match) {
- return match[1];
- } else {
- return null;
- }
- }
- },
- setup: function(input) {
+ setup(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
- this.input = input || $('.js-gfm-input');
+ this.input = $(input) || $('.js-gfm-input');
this.setupLifecycle();
- },
+ }
+
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
@@ -114,47 +29,127 @@ window.gl.GfmAutoComplete = {
// Needed for slash commands with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
});
- },
- setupAtWho: function($input) {
+ }
+
+ setupAtWho($input) {
+ this.setupEmoji($input);
+ this.setupMembers($input);
+ this.setupIssues($input);
+ this.setupMilestones($input);
+ this.setupMergeRequests($input);
+ this.setupLabels($input);
+
+ const fetchData = this.fetchData.bind(this);
+ // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+ $input.filter('[data-supports-slash-commands="true"]').atwho({
+ at: '/',
+ alias: 'commands',
+ searchKey: 'search',
+ skipSpecialCharacterTest: true,
+ data: GfmAutoComplete.defaultLoadingData,
+ displayTpl(value) {
+ if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
+ var tpl = '<li>/${name}';
+ if (value.aliases.length > 0) {
+ tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ }
+ if (value.params.length > 0) {
+ tpl += ' <small><%- params.join(" ") %></small>';
+ }
+ if (value.description !== '') {
+ tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ tpl += '</li>';
+ return _.template(tpl)(value);
+ },
+ insertTpl: function(value) {
+ var tpl = "/${name} ";
+ var reference_prefix = null;
+ if (value.params.length > 0) {
+ reference_prefix = value.params[0][0];
+ if (/^[@%~]/.test(reference_prefix)) {
+ tpl += '<%- reference_prefix %>';
+ }
+ }
+ return _.template(tpl)({ reference_prefix: reference_prefix });
+ },
+ suffix: '',
+ callbacks: {
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ beforeSave(commands) {
+ if (GfmAutoComplete.isLoading(commands)) return commands;
+ return $.map(commands, function(c) {
+ var search = c.name;
+ if (c.aliases.length > 0) {
+ search = search + " " + c.aliases.join(" ");
+ }
+ return {
+ name: c.name,
+ aliases: c.aliases,
+ params: c.params,
+ description: c.description,
+ search: search
+ };
+ });
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
+ var match = regexp.exec(subtext);
+ if (match) {
+ return match[1];
+ } else {
+ return null;
+ }
+ }
+ }
+ });
+ }
+
+ setupEmoji($input) {
+ const fetchData = this.fetchData.bind(this);
// Emoji
$input.atwho({
at: ':',
- displayTpl: function(value) {
- return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
- }.bind(this),
+ displayTpl(value) {
+ return value && value.name ? GfmAutoComplete.Emoji.templateFunction(value.name) : GfmAutoComplete.Loading.template;
+ },
insertTpl: ':${name}:',
skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
+ data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
-
- matcher: (flag, subtext) => {
- const relevantText = subtext.trim().split(/\s/).pop();
- const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
- const match = regexp.exec(relevantText);
-
- return match && match.length ? match[1] : null;
- }
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
}
});
+ }
+
+ setupMembers($input) {
+ const fetchData = this.fetchData.bind(this);
// Team Members
$input.atwho({
at: '@',
- displayTpl: function(value) {
- return value.username != null ? this.Members.template : this.Loading.template;
- }.bind(this),
+ displayTpl(value) {
+ return value.username != null ? GfmAutoComplete.Members.template : GfmAutoComplete.Loading.template;
+ },
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
+ data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ matcher: GfmAutoComplete.DefaultOptions.matcher,
beforeSave: function(members) {
return $.map(members, function(m) {
let title = '';
@@ -180,20 +175,26 @@ window.gl.GfmAutoComplete = {
}
}
});
+ }
+
+ setupIssues($input) {
+ const fetchData = this.fetchData.bind(this);
$input.atwho({
at: '#',
alias: 'issues',
searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
+ displayTpl(value) {
+ return value.title != null ? GfmAutoComplete.Issues.template : GfmAutoComplete.Loading.template;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ matcher: GfmAutoComplete.DefaultOptions.matcher,
beforeSave: function(issues) {
return $.map(issues, function(i) {
if (i.title == null) {
@@ -208,20 +209,26 @@ window.gl.GfmAutoComplete = {
}
}
});
+ }
+
+ setupMilestones($input) {
+ const fetchData = this.fetchData.bind(this);
$input.atwho({
at: '%',
alias: 'milestones',
searchKey: 'search',
insertTpl: '${atwho-at}${title}',
- displayTpl: function(value) {
- return value.title != null ? this.Milestones.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
+ displayTpl(value) {
+ return value.title != null ? GfmAutoComplete.Milestones.template : GfmAutoComplete.Loading.template;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
callbacks: {
- matcher: this.DefaultOptions.matcher,
- sorter: this.DefaultOptions.sorter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
+ matcher: GfmAutoComplete.DefaultOptions.matcher,
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
@@ -236,20 +243,26 @@ window.gl.GfmAutoComplete = {
}
}
});
+ }
+
+ setupMergeRequests($input) {
+ const fetchData = this.fetchData.bind(this);
$input.atwho({
at: '!',
alias: 'mergerequests',
searchKey: 'search',
- displayTpl: function(value) {
- return value.title != null ? this.Issues.template : this.Loading.template;
- }.bind(this),
- data: this.defaultLoadingData,
+ displayTpl(value) {
+ return value.title != null ? GfmAutoComplete.Issues.template : GfmAutoComplete.Loading.template;
+ },
+ data: GfmAutoComplete.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- matcher: this.DefaultOptions.matcher,
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ matcher: GfmAutoComplete.DefaultOptions.matcher,
beforeSave: function(merges) {
return $.map(merges, function(m) {
if (m.title == null) {
@@ -264,22 +277,28 @@ window.gl.GfmAutoComplete = {
}
}
});
+ }
+
+ setupLabels($input) {
+ const fetchData = this.fetchData.bind(this);
$input.atwho({
at: '~',
alias: 'labels',
searchKey: 'search',
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- return this.isLoading(value) ? this.Loading.template : this.Labels.template;
- }.bind(this),
+ data: GfmAutoComplete.defaultLoadingData,
+ displayTpl(value) {
+ return GfmAutoComplete.isLoading(value) ? GfmAutoComplete.Loading.template : GfmAutoComplete.Labels.template;
+ },
insertTpl: '${atwho-at}${title}',
callbacks: {
- matcher: this.DefaultOptions.matcher,
- beforeInsert: this.DefaultOptions.beforeInsert,
- filter: this.DefaultOptions.filter,
- sorter: this.DefaultOptions.sorter,
- beforeSave: function(merges) {
- if (gl.GfmAutoComplete.isLoading(merges)) return merges;
+ matcher: GfmAutoComplete.DefaultOptions.matcher,
+ beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert,
+ filter(...args) {
+ return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args);
+ },
+ sorter: GfmAutoComplete.DefaultOptions.sorter,
+ beforeSave(merges) {
+ if (GfmAutoComplete.isLoading(merges)) return merges;
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
@@ -295,105 +314,132 @@ window.gl.GfmAutoComplete = {
search: "" + m.title
};
});
- }
- }
- });
- // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
- $input.filter('[data-supports-slash-commands="true"]').atwho({
- at: '/',
- alias: 'commands',
- searchKey: 'search',
- skipSpecialCharacterTest: true,
- data: this.defaultLoadingData,
- displayTpl: function(value) {
- if (this.isLoading(value)) return this.Loading.template;
- var tpl = '<li>/${name}';
- if (value.aliases.length > 0) {
- tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
- }
- if (value.params.length > 0) {
- tpl += ' <small><%- params.join(" ") %></small>';
- }
- if (value.description !== '') {
- tpl += '<small class="description"><i><%- description %></i></small>';
- }
- tpl += '</li>';
- return _.template(tpl)(value);
- }.bind(this),
- insertTpl: function(value) {
- var tpl = "/${name} ";
- var reference_prefix = null;
- if (value.params.length > 0) {
- reference_prefix = value.params[0][0];
- if (/^[@%~]/.test(reference_prefix)) {
- tpl += '<%- reference_prefix %>';
- }
- }
- return _.template(tpl)({ reference_prefix: reference_prefix });
- },
- suffix: '',
- callbacks: {
- sorter: this.DefaultOptions.sorter,
- filter: this.DefaultOptions.filter,
- beforeInsert: this.DefaultOptions.beforeInsert,
- beforeSave: function(commands) {
- if (gl.GfmAutoComplete.isLoading(commands)) return commands;
- return $.map(commands, function(c) {
- var search = c.name;
- if (c.aliases.length > 0) {
- search = search + " " + c.aliases.join(" ");
- }
- return {
- name: c.name,
- aliases: c.aliases,
- params: c.params,
- description: c.description,
- search: search
- };
- });
},
- matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
- var match = regexp.exec(subtext);
- if (match) {
- return match[1];
- } else {
- return null;
- }
- }
}
});
- return;
- },
- fetchData: function($input, at) {
+ }
+
+ fetchData($input, at) {
if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true;
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
- } else if (this.atTypeMap[at] === 'emojis') {
+ } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
} else {
- $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+ $.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => {
this.loadData($input, at, data);
}).fail(() => { this.isLoadingData[at] = false; });
}
- },
- loadData: function($input, at, data) {
+ }
+ loadData($input, at, data) {
this.isLoadingData[at] = false;
this.cachedData[at] = data;
$input.atwho('load', at, data);
// This trigger at.js again
// otherwise we would be stuck with loading until the user types
return $input.trigger('keyup');
- },
- isLoading(data) {
+ }
+
+ static isLoading(data) {
var dataToInspect = data;
if (data && data.length > 0) {
dataToInspect = data[0];
}
- var loadingState = this.defaultLoadingData[0];
+ var loadingState = GfmAutoComplete.defaultLoadingData[0];
return dataToInspect &&
(dataToInspect === loadingState || dataToInspect.name === loadingState);
}
+}
+
+GfmAutoComplete.defaultLoadingData = ['loading'];
+
+GfmAutoComplete.DefaultOptions = {
+ sorter(query, items, searchKey) {
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
+ if (GfmAutoComplete.isLoading(items)) {
+ this.setting.highlightFirst = false;
+ return items;
+ }
+ return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
+ },
+ filter(fetchData, query, data, searchKey) {
+ if (GfmAutoComplete.isLoading(data)) {
+ fetchData(this.$inputor, this.at);
+ return data;
+ } else {
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
+ }
+ },
+ beforeInsert(value) {
+ if (value && !this.setting.skipSpecialCharacterTest) {
+ var withoutAt = value.substring(1);
+ if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
+ }
+ return value;
+ },
+ matcher(flag, subtext) {
+ // The below is taken from At.js source
+ // Tweaked to commands to start without a space only if char before is a non-word character
+ // https://github.com/ichord/At.js
+ var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
+ atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
+ atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
+ subtext = subtext.split(/\s+/g).pop();
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+
+ regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
+
+ match = regexp.exec(subtext);
+
+ if (match) {
+ return match[1];
+ } else {
+ return null;
+ }
+ }
+};
+
+GfmAutoComplete.atTypeMap = {
+ ':': 'emojis',
+ '@': 'members',
+ '#': 'issues',
+ '!': 'mergeRequests',
+ '~': 'labels',
+ '%': 'milestones',
+ '/': 'commands'
+};
+
+// Emoji
+GfmAutoComplete.Emoji = {
+ templateFunction: function(name) {
+ return `<li>
+ ${name} ${glEmojiTag(name)}
+ </li>
+ `;
+ }
};
+// Team Members
+GfmAutoComplete.Members = {
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
+};
+GfmAutoComplete.Labels = {
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+};
+// Issues and MergeRequests
+GfmAutoComplete.Issues = {
+ template: '<li><small>${id}</small> ${title}</li>'
+};
+// Milestones
+GfmAutoComplete.Milestones = {
+ template: '<li>${title}</li>'
+};
+GfmAutoComplete.Loading = {
+ template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>'
+};
+
+export default GfmAutoComplete;
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index ff10f19a4fe..b40b053c9ca 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,6 +3,8 @@
/* global DropzoneInput */
/* global autosize */
+import GfmAutoComplete from './gfm_auto_complete';
+
window.gl = window.gl || {};
function GLForm(form) {
@@ -31,7 +33,7 @@ GLForm.prototype.setupForm = function() {
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
- gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
// form and textarea event listeners
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 687c2bb6110..81d70cde9a2 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -6,6 +6,8 @@
/* global dateFormat */
/* global Pikaday */
+import GfmAutoComplete from './gfm_auto_complete';
+
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -21,7 +23,7 @@
this.renderWipExplanation = bind(this.renderWipExplanation, this);
this.resetAutosave = bind(this.resetAutosave, this);
this.handleSubmit = bind(this.handleSubmit, this);
- gl.GfmAutoComplete.setup();
+ new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index be3c2c9fbb1..36b8c79105b 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -97,7 +97,6 @@ import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import './flash';
-import './gfm_auto_complete';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 974fb0d83da..5fd79b73d32 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -11,7 +11,6 @@ require('./autosave');
window.autosize = require('vendor/autosize');
window.Dropzone = require('dropzone');
require('./dropzone_input');
-require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
require('./task_list');
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 769f6fb0151..6caaba240bb 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,7 @@
- if project
:javascript
+ gl.GfmAutoComplete = gl.GfmAutoComplete || {};
gl.GfmAutoComplete.dataSources = {
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
@@ -11,5 +12,3 @@
milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
};
-
- gl.GfmAutoComplete.setup();
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 36543edc040..2805f73d4ea 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -3,9 +3,9 @@
= render "layouts/head"
%body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
+ = render "layouts/init_auto_complete" if @gfm_form
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
- = render "layouts/init_auto_complete" if @gfm_form
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index dd9622f16a0..67bc9142356 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do
end
it 'does not load on project#show' do
- expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
+ expect(evaluate_script('gl.GfmAutoComplete')).to eq(nil)
end
it 'loads on new issue page' do
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index 5dfa4008fbd..7f388a42e2b 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -1,12 +1,10 @@
/* eslint no-param-reassign: "off" */
-require('~/gfm_auto_complete');
+import GfmAutoComplete from '~/gfm_auto_complete';
+
require('vendor/jquery.caret');
require('vendor/jquery.atwho');
-const global = window.gl || (window.gl = {});
-const GfmAutoComplete = global.GfmAutoComplete;
-
describe('GfmAutoComplete', function () {
describe('DefaultOptions.sorter', function () {
describe('assets loading', function () {