From 5c34323a5cfab926d6d5e89b3caba5f0f6944f7f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 3 May 2017 08:18:14 -0500 Subject: [PATCH 1/3] Make `gfm_auto_complete` a module and fix up tech debt Spawned from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1739 Conflicts: app/assets/javascripts/dispatcher.js app/assets/javascripts/issuable_form.js --- app/assets/javascripts/dispatcher.js | 3 + app/assets/javascripts/gfm_auto_complete.js | 373 ++++++++++-------- app/assets/javascripts/gl_form.js | 4 +- app/assets/javascripts/issuable_form.js | 4 +- app/assets/javascripts/main.js | 1 - app/assets/javascripts/notes.js | 1 - .../layouts/_init_auto_complete.html.haml | 3 +- app/views/layouts/application.html.haml | 2 +- .../projects/gfm_autocomplete_load_spec.rb | 2 +- spec/javascripts/gfm_auto_complete_spec.js | 6 +- 10 files changed, 214 insertions(+), 185 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 16ac14a64ae718..87007725e53c5d 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -55,6 +55,7 @@ import BlobViewer from './blob/viewer/index'; import GeoNodes from './geo_nodes'; import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; +import GfmAutoComplete from './gfm_auto_complete'; const ShortcutsBlob = require('./shortcuts_blob'); @@ -81,6 +82,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 f1b99023c723ca..96ca2298a3b720 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -5,103 +5,18 @@ 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 `
  • - ${name} ${glEmojiTag(name)} -
  • - `; - } - }, - // Team Members - Members: { - template: '
  • ${avatarTag} ${username} ${title}
  • ' - }, - Labels: { - template: '
  • ${title}
  • ' - }, - // Issues and MergeRequests - Issues: { - template: '
  • ${id} ${title}
  • ' - }, - // Milestones - Milestones: { - template: '
  • ${title}
  • ' - }, - Loading: { - template: '
  • Loading...
  • ' - }, - 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, enableMap = { + setup(input, enableMap = { emojis: true, members: true, issues: true, @@ -113,7 +28,8 @@ window.gl.GfmAutoComplete = { this.input = input || $('.js-gfm-input'); this.enableMap = enableMap; this.setupLifecycle(); - }, + } + setupLifecycle() { this.input.each((i, input) => { const $input = $(input); @@ -122,9 +38,9 @@ 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) { if (this.enableMap.emojis) this.setupEmoji($input); if (this.enableMap.members) this.setupMembers($input); if (this.enableMap.issues) this.setupIssues($input); @@ -132,15 +48,16 @@ window.gl.GfmAutoComplete = { if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) 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: this.defaultLoadingData, - displayTpl: function(value) { - if (this.isLoading(value)) return this.Loading.template; + data: GfmAutoComplete.defaultLoadingData, + displayTpl(value) { + if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; var tpl = '
  • /${name}'; if (value.aliases.length > 0) { tpl += ' (or /<%- aliases.join(", /") %>)'; @@ -153,7 +70,7 @@ window.gl.GfmAutoComplete = { } tpl += '
  • '; return _.template(tpl)(value); - }.bind(this), + }, insertTpl: function(value) { var tpl = "/${name} "; var reference_prefix = null; @@ -167,11 +84,13 @@ window.gl.GfmAutoComplete = { }, suffix: '', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(commands) { - if (gl.GfmAutoComplete.isLoading(commands)) return commands; + 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) { @@ -197,52 +116,56 @@ window.gl.GfmAutoComplete = { } } }); - return; - }, + } 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) => { + sorter: GfmAutoComplete.DefaultOptions.sorter, + beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, + filter(...args) { + return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); + }, + 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; - } + }, } }); - }, + } 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 = ''; @@ -268,23 +191,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) { @@ -299,23 +225,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) { @@ -330,23 +259,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) { @@ -361,25 +293,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)) { @@ -395,40 +330,132 @@ window.gl.GfmAutoComplete = { search: "" + m.title }; }); - } + }, } }); - }, + } - 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 `
  • + ${name} ${glEmojiTag(name)} +
  • + `; + } +}; +// Team Members +GfmAutoComplete.Members = { + template: '
  • ${avatarTag} ${username} ${title}
  • ' +}; +GfmAutoComplete.Labels = { + template: '
  • ${title}
  • ' +}; +// Issues and MergeRequests +GfmAutoComplete.Issues = { + template: '
  • ${id} ${title}
  • ' +}; +// Milestones +GfmAutoComplete.Milestones = { + template: '
  • ${title}
  • ' +}; +GfmAutoComplete.Loading = { + template: '
  • Loading...
  • ' +}; + +export default GfmAutoComplete; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index ff06092e4d6eba..51822f21e66e12 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); } diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 8448cb15651bbe..6a5a44ce7ae514 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -7,6 +7,8 @@ /* global dateFormat */ /* global Pikaday */ +import GfmAutoComplete from './gfm_auto_complete'; + (function() { this.IssuableForm = (function() { IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; @@ -20,7 +22,7 @@ this.renderWipExplanation = this.renderWipExplanation.bind(this); this.resetAutosave = this.resetAutosave.bind(this); this.handleSubmit = this.handleSubmit.bind(this); - gl.GfmAutoComplete.setup(); + new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); new UsersSelect(); new GroupsSelect(); new ZenMode(); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index abacab9b4c31de..5b4d5a04197b82 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 9181451bbb8b2a..a207f256f8963c 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -12,7 +12,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 769f6fb01512a4..6caaba240bb489 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 7e011ac3e75b78..03688e9ff21dff 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,8 +2,8 @@ %html{ lang: I18n.locale, class: "#{page_class}" } = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } + = 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 dd9622f16a0c43..67bc9142356fc2 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -10,7 +10,7 @@ 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 5dfa4008fbd559..7f388a42e2b004 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 () { -- GitLab From 6605f80b6c095f0ed2323a367475d0c479025f47 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 12 May 2017 00:53:32 -0500 Subject: [PATCH 2/3] Remove eslint-disable from gfm_auto_complete --- app/assets/javascripts/gfm_auto_complete.js | 211 +++++++++++--------- 1 file changed, 117 insertions(+), 94 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 96ca2298a3b720..5e1d22e0da3b48 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,5 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ - import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; @@ -22,7 +20,7 @@ class GfmAutoComplete { issues: true, milestones: true, mergeRequests: true, - labels: true + labels: true, }) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); @@ -58,7 +56,8 @@ class GfmAutoComplete { data: GfmAutoComplete.defaultLoadingData, displayTpl(value) { if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; - var tpl = '
  • /${name}'; + // eslint-disable-next-line no-template-curly-in-string + let tpl = '
  • /${name}'; if (value.aliases.length > 0) { tpl += ' (or /<%- aliases.join(", /") %>)'; } @@ -71,16 +70,17 @@ class GfmAutoComplete { tpl += '
  • '; return _.template(tpl)(value); }, - insertTpl: function(value) { - var tpl = "/${name} "; - var reference_prefix = null; + insertTpl(value) { + // eslint-disable-next-line no-template-curly-in-string + let tpl = '/${name} '; + let referencePrefix = null; if (value.params.length > 0) { - reference_prefix = value.params[0][0]; - if (/^[@%~]/.test(reference_prefix)) { - tpl += '<%- reference_prefix %>'; + referencePrefix = value.params[0][0]; + if (/^[@%~]/.test(referencePrefix)) { + tpl += '<%- referencePrefix %>'; } } - return _.template(tpl)({ reference_prefix: reference_prefix }); + return _.template(tpl)({ reference_prefix: referencePrefix }); }, suffix: '', callbacks: { @@ -91,30 +91,29 @@ class GfmAutoComplete { beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, beforeSave(commands) { if (GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, function(c) { - var search = c.name; + return $.map(commands, (c) => { + let search = c.name; if (c.aliases.length > 0) { - search = search + " " + c.aliases.join(" "); + search = `${search} ${c.aliases.join(' ')}`; } return { name: c.name, aliases: c.aliases, params: c.params, description: c.description, - search: search + search, }; }); }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; - var match = regexp.exec(subtext); + matcher(flag, subtext) { + const regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; + const match = regexp.exec(subtext); if (match) { return match[1]; - } else { - return null; } - } - } + return null; + }, + }, }); } @@ -124,8 +123,13 @@ class GfmAutoComplete { $input.atwho({ at: ':', displayTpl(value) { - return value && value.name ? GfmAutoComplete.Emoji.templateFunction(value.name) : GfmAutoComplete.Loading.template; + let tmpl = GfmAutoComplete.Loading.template; + if (value && value.name) { + tmpl = GfmAutoComplete.Emoji.templateFunction(value.name); + } + return tmpl; }, + // eslint-disable-next-line no-template-curly-in-string insertTpl: ':${name}:', skipSpecialCharacterTest: true, data: GfmAutoComplete.defaultLoadingData, @@ -142,7 +146,7 @@ class GfmAutoComplete { return match && match.length ? match[1] : null; }, - } + }, }); } @@ -152,8 +156,13 @@ class GfmAutoComplete { $input.atwho({ at: '@', displayTpl(value) { - return value.username != null ? GfmAutoComplete.Members.template : GfmAutoComplete.Loading.template; + let tmpl = GfmAutoComplete.Loading.template; + if (value.username != null) { + tmpl = GfmAutoComplete.Members.template; + } + return tmpl; }, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${username}', searchKey: 'search', alwaysHighlightFirst: true, @@ -166,15 +175,15 @@ class GfmAutoComplete { }, beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, matcher: GfmAutoComplete.DefaultOptions.matcher, - beforeSave: function(members) { - return $.map(members, function(m) { + beforeSave(members) { + return $.map(members, (m) => { let title = ''; if (m.username == null) { return m; } title = m.name; if (m.count) { - title += " (" + m.count + ")"; + title += ` (${m.count})`; } const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); @@ -185,11 +194,11 @@ class GfmAutoComplete { username: m.username, avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, title: sanitize(title), - search: sanitize(m.username + " " + m.name) + search: sanitize(`${m.username} ${m.name}`), }; }); - } - } + }, + }, }); } @@ -200,9 +209,14 @@ class GfmAutoComplete { alias: 'issues', searchKey: 'search', displayTpl(value) { - return value.title != null ? GfmAutoComplete.Issues.template : GfmAutoComplete.Loading.template; + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Issues.template; + } + return tmpl; }, data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${id}', callbacks: { sorter: GfmAutoComplete.DefaultOptions.sorter, @@ -211,19 +225,19 @@ class GfmAutoComplete { }, beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, matcher: GfmAutoComplete.DefaultOptions.matcher, - beforeSave: function(issues) { - return $.map(issues, function(i) { + beforeSave(issues) { + return $.map(issues, (i) => { if (i.title == null) { return i; } return { id: i.iid, title: sanitize(i.title), - search: i.iid + " " + i.title + search: `${i.iid} ${i.title}`, }; }); - } - } + }, + }, }); } @@ -233,9 +247,14 @@ class GfmAutoComplete { at: '%', alias: 'milestones', searchKey: 'search', + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${title}', displayTpl(value) { - return value.title != null ? GfmAutoComplete.Milestones.template : GfmAutoComplete.Loading.template; + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Milestones.template; + } + return tmpl; }, data: GfmAutoComplete.defaultLoadingData, callbacks: { @@ -245,19 +264,19 @@ class GfmAutoComplete { filter(...args) { return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); }, - beforeSave: function(milestones) { - return $.map(milestones, function(m) { + beforeSave(milestones) { + return $.map(milestones, (m) => { if (m.title == null) { return m; } return { id: m.iid, title: sanitize(m.title), - search: "" + m.title + search: m.title, }; }); - } - } + }, + }, }); } @@ -268,9 +287,14 @@ class GfmAutoComplete { alias: 'mergerequests', searchKey: 'search', displayTpl(value) { - return value.title != null ? GfmAutoComplete.Issues.template : GfmAutoComplete.Loading.template; + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Issues.template; + } + return tmpl; }, data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${id}', callbacks: { sorter: GfmAutoComplete.DefaultOptions.sorter, @@ -279,19 +303,19 @@ class GfmAutoComplete { }, beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, matcher: GfmAutoComplete.DefaultOptions.matcher, - beforeSave: function(merges) { - return $.map(merges, function(m) { + beforeSave(merges) { + return $.map(merges, (m) => { if (m.title == null) { return m; } return { id: m.iid, title: sanitize(m.title), - search: m.iid + " " + m.title + search: `${m.iid} ${m.title}`, }; }); - } - } + }, + }, }); } @@ -303,8 +327,13 @@ class GfmAutoComplete { searchKey: 'search', data: GfmAutoComplete.defaultLoadingData, displayTpl(value) { - return GfmAutoComplete.isLoading(value) ? GfmAutoComplete.Loading.template : GfmAutoComplete.Labels.template; + let tmpl = GfmAutoComplete.Labels.template; + if (GfmAutoComplete.isLoading(value)) { + tmpl = GfmAutoComplete.Loading.template; + } + return tmpl; }, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${title}', callbacks: { matcher: GfmAutoComplete.DefaultOptions.matcher, @@ -315,23 +344,13 @@ class GfmAutoComplete { sorter: GfmAutoComplete.DefaultOptions.sorter, beforeSave(merges) { if (GfmAutoComplete.isLoading(merges)) return merges; - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } - }; - return $.map(merges, function(m) { - return { - title: sanitize(m.title), - color: m.color, - search: "" + m.title - }; - }); + return $.map(merges, m => ({ + title: sanitize(m.title), + color: m.color, + search: m.title, + })); }, - } + }, }); } @@ -358,12 +377,12 @@ class GfmAutoComplete { } static isLoading(data) { - var dataToInspect = data; + let dataToInspect = data; if (data && data.length > 0) { dataToInspect = data[0]; } - var loadingState = GfmAutoComplete.defaultLoadingData[0]; + const loadingState = GfmAutoComplete.defaultLoadingData[0]; return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState); } @@ -378,46 +397,46 @@ GfmAutoComplete.DefaultOptions = { this.setting.highlightFirst = false; return items; } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); + 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); } + return $.fn.atwho.default.callbacks.filter(query, data, searchKey); }, beforeInsert(value) { + let resultantValue = value; if (value && !this.setting.skipSpecialCharacterTest) { - var withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; + const withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) { + resultantValue = `${value.charAt()}"${withoutAt}"`; + } } - return value; + return resultantValue; }, 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, "\\$&"); + const atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + const atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + const targetSubtext = subtext.split(/\s+/g).pop(); + const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); + const accentAChar = decodeURI('%C3%80'); + const accentYChar = 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'); + const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); - match = regexp.exec(subtext); + const match = regexp.exec(targetSubtext); if (match) { return match[1]; - } else { - return null; } - } + return null; + }, }; GfmAutoComplete.atTypeMap = { @@ -427,35 +446,39 @@ GfmAutoComplete.atTypeMap = { '!': 'mergeRequests', '~': 'labels', '%': 'milestones', - '/': 'commands' + '/': 'commands', }; // Emoji GfmAutoComplete.Emoji = { - templateFunction: function(name) { + templateFunction(name) { return `
  • ${name} ${glEmojiTag(name)}
  • `; - } + }, }; // Team Members GfmAutoComplete.Members = { - template: '
  • ${avatarTag} ${username} ${title}
  • ' + // eslint-disable-next-line no-template-curly-in-string + template: '
  • ${avatarTag} ${username} ${title}
  • ', }; GfmAutoComplete.Labels = { - template: '
  • ${title}
  • ' + // eslint-disable-next-line no-template-curly-in-string + template: '
  • ${title}
  • ', }; // Issues and MergeRequests GfmAutoComplete.Issues = { - template: '
  • ${id} ${title}
  • ' + // eslint-disable-next-line no-template-curly-in-string + template: '
  • ${id} ${title}
  • ', }; // Milestones GfmAutoComplete.Milestones = { - template: '
  • ${title}
  • ' + // eslint-disable-next-line no-template-curly-in-string + template: '
  • ${title}
  • ', }; GfmAutoComplete.Loading = { - template: '
  • Loading...
  • ' + template: '
  • Loading...
  • ', }; export default GfmAutoComplete; -- GitLab From 13003cad556aa5d1c6355410f14470bf56c56f17 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 12 May 2017 01:14:55 -0500 Subject: [PATCH 3/3] Dry up callbacks and saner this --- app/assets/javascripts/gfm_auto_complete.js | 158 ++++++++------------ spec/javascripts/gfm_auto_complete_spec.js | 14 +- 2 files changed, 70 insertions(+), 102 deletions(-) diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 5e1d22e0da3b48..b8a923cf6190bd 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -46,7 +46,6 @@ class GfmAutoComplete { if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) 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: '/', @@ -80,15 +79,11 @@ class GfmAutoComplete { tpl += '<%- referencePrefix %>'; } } - return _.template(tpl)({ reference_prefix: referencePrefix }); + return _.template(tpl)({ referencePrefix }); }, suffix: '', callbacks: { - sorter: GfmAutoComplete.DefaultOptions.sorter, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, + ...this.getDefaultCallbacks(), beforeSave(commands) { if (GfmAutoComplete.isLoading(commands)) return commands; return $.map(commands, (c) => { @@ -118,7 +113,6 @@ class GfmAutoComplete { } setupEmoji($input) { - const fetchData = this.fetchData.bind(this); // Emoji $input.atwho({ at: ':', @@ -134,11 +128,7 @@ class GfmAutoComplete { skipSpecialCharacterTest: true, data: GfmAutoComplete.defaultLoadingData, callbacks: { - sorter: GfmAutoComplete.DefaultOptions.sorter, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, + ...this.getDefaultCallbacks(), matcher(flag, subtext) { const relevantText = subtext.trim().split(/\s/).pop(); const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); @@ -151,7 +141,6 @@ class GfmAutoComplete { } setupMembers($input) { - const fetchData = this.fetchData.bind(this); // Team Members $input.atwho({ at: '@', @@ -169,12 +158,7 @@ class GfmAutoComplete { skipSpecialCharacterTest: true, data: GfmAutoComplete.defaultLoadingData, callbacks: { - sorter: GfmAutoComplete.DefaultOptions.sorter, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, - matcher: GfmAutoComplete.DefaultOptions.matcher, + ...this.getDefaultCallbacks(), beforeSave(members) { return $.map(members, (m) => { let title = ''; @@ -203,7 +187,6 @@ class GfmAutoComplete { } setupIssues($input) { - const fetchData = this.fetchData.bind(this); $input.atwho({ at: '#', alias: 'issues', @@ -219,12 +202,7 @@ class GfmAutoComplete { // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${id}', callbacks: { - sorter: GfmAutoComplete.DefaultOptions.sorter, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, - matcher: GfmAutoComplete.DefaultOptions.matcher, + ...this.getDefaultCallbacks(), beforeSave(issues) { return $.map(issues, (i) => { if (i.title == null) { @@ -242,7 +220,6 @@ class GfmAutoComplete { } setupMilestones($input) { - const fetchData = this.fetchData.bind(this); $input.atwho({ at: '%', alias: 'milestones', @@ -258,12 +235,7 @@ class GfmAutoComplete { }, data: GfmAutoComplete.defaultLoadingData, callbacks: { - matcher: GfmAutoComplete.DefaultOptions.matcher, - sorter: GfmAutoComplete.DefaultOptions.sorter, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, + ...this.getDefaultCallbacks(), beforeSave(milestones) { return $.map(milestones, (m) => { if (m.title == null) { @@ -281,7 +253,6 @@ class GfmAutoComplete { } setupMergeRequests($input) { - const fetchData = this.fetchData.bind(this); $input.atwho({ at: '!', alias: 'mergerequests', @@ -297,12 +268,7 @@ class GfmAutoComplete { // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${id}', callbacks: { - sorter: GfmAutoComplete.DefaultOptions.sorter, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, - matcher: GfmAutoComplete.DefaultOptions.matcher, + ...this.getDefaultCallbacks(), beforeSave(merges) { return $.map(merges, (m) => { if (m.title == null) { @@ -320,7 +286,6 @@ class GfmAutoComplete { } setupLabels($input) { - const fetchData = this.fetchData.bind(this); $input.atwho({ at: '~', alias: 'labels', @@ -336,12 +301,7 @@ class GfmAutoComplete { // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${title}', callbacks: { - matcher: GfmAutoComplete.DefaultOptions.matcher, - beforeInsert: GfmAutoComplete.DefaultOptions.beforeInsert, - filter(...args) { - return GfmAutoComplete.DefaultOptions.filter.bind(this)(fetchData, ...args); - }, - sorter: GfmAutoComplete.DefaultOptions.sorter, + ...this.getDefaultCallbacks(), beforeSave(merges) { if (GfmAutoComplete.isLoading(merges)) return merges; return $.map(merges, m => ({ @@ -354,6 +314,59 @@ class GfmAutoComplete { }); } + getDefaultCallbacks() { + const fetchData = this.fetchData.bind(this); + + return { + 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(query, data, searchKey) { + if (GfmAutoComplete.isLoading(data)) { + fetchData(this.$inputor, this.at); + return data; + } + return $.fn.atwho.default.callbacks.filter(query, data, searchKey); + }, + beforeInsert(value) { + let resultantValue = value; + if (value && !this.setting.skipSpecialCharacterTest) { + const withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) { + resultantValue = `${value.charAt()}"${withoutAt}"`; + } + } + return resultantValue; + }, + 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 + const atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + const atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + const targetSubtext = subtext.split(/\s+/g).pop(); + const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); + + const accentAChar = decodeURI('%C3%80'); + const accentYChar = decodeURI('%C3%BF'); + + const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + + const match = regexp.exec(targetSubtext); + + if (match) { + return match[1]; + } + return null; + }, + }; + } + fetchData($input, at) { if (this.isLoadingData[at]) return; this.isLoadingData[at] = true; @@ -390,55 +403,6 @@ class GfmAutoComplete { 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; - } - return $.fn.atwho.default.callbacks.filter(query, data, searchKey); - }, - beforeInsert(value) { - let resultantValue = value; - if (value && !this.setting.skipSpecialCharacterTest) { - const withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) { - resultantValue = `${value.charAt()}"${withoutAt}"`; - } - } - return resultantValue; - }, - 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 - const atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - const atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - const targetSubtext = subtext.split(/\s+/g).pop(); - const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); - - const accentAChar = decodeURI('%C3%80'); - const accentYChar = decodeURI('%C3%BF'); - - const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); - - const match = regexp.exec(targetSubtext); - - if (match) { - return match[1]; - } - return null; - }, -}; - GfmAutoComplete.atTypeMap = { ':': 'emojis', '@': 'members', diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index 7f388a42e2b004..d0f15c902b5522 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -6,6 +6,10 @@ require('vendor/jquery.caret'); require('vendor/jquery.atwho'); describe('GfmAutoComplete', function () { + const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ + fetchData: () => {}, + }); + describe('DefaultOptions.sorter', function () { describe('assets loading', function () { beforeEach(function () { @@ -14,7 +18,7 @@ describe('GfmAutoComplete', function () { this.atwhoInstance = { setting: {} }; this.items = []; - this.sorterValue = GfmAutoComplete.DefaultOptions.sorter + this.sorterValue = gfmAutoCompleteCallbacks.sorter .call(this.atwhoInstance, '', this.items); }); @@ -36,7 +40,7 @@ describe('GfmAutoComplete', function () { it('should enable highlightFirst if alwaysHighlightFirst is set', function () { const atwhoInstance = { setting: { alwaysHighlightFirst: true } }; - GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance); + gfmAutoCompleteCallbacks.sorter.call(atwhoInstance); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); @@ -44,7 +48,7 @@ describe('GfmAutoComplete', function () { it('should enable highlightFirst if a query is present', function () { const atwhoInstance = { setting: {} }; - GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query'); + gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query'); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); @@ -56,7 +60,7 @@ describe('GfmAutoComplete', function () { const items = []; const searchKey = 'searchKey'; - GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey); + gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey); }); @@ -65,7 +69,7 @@ describe('GfmAutoComplete', function () { describe('DefaultOptions.matcher', function () { const defaultMatcher = (context, flag, subtext) => ( - GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext) + gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) ); const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; -- GitLab