Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
b8b6de4c
Commit
b8b6de4c
authored
May 16, 2017
by
Eric Eastwood
Committed by
Clement Ho
May 16, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Make `gfm_auto_complete` into a module and fix up tech debt
parent
dc045dab
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
270 additions
and
251 deletions
+270
-251
app/assets/javascripts/dispatcher.js
app/assets/javascripts/dispatcher.js
+3
-0
app/assets/javascripts/gfm_auto_complete.js
app/assets/javascripts/gfm_auto_complete.js
+248
-234
app/assets/javascripts/gl_form.js
app/assets/javascripts/gl_form.js
+3
-1
app/assets/javascripts/issuable_form.js
app/assets/javascripts/issuable_form.js
+2
-1
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+0
-1
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+0
-1
app/views/layouts/_init_auto_complete.html.haml
app/views/layouts/_init_auto_complete.html.haml
+1
-2
app/views/layouts/application.html.haml
app/views/layouts/application.html.haml
+1
-1
spec/features/projects/gfm_autocomplete_load_spec.rb
spec/features/projects/gfm_autocomplete_load_spec.rb
+1
-1
spec/javascripts/gfm_auto_complete_spec.js
spec/javascripts/gfm_auto_complete_spec.js
+11
-9
No files found.
app/assets/javascripts/dispatcher.js
View file @
b8b6de4c
...
...
@@ -53,6 +53,7 @@ import BlobViewer from './blob/viewer/index';
import
AutoWidthDropdownSelect
from
'
./issuable/auto_width_dropdown_select
'
;
import
UsersSelect
from
'
./users_select
'
;
import
RefSelectDropdown
from
'
./ref_select_dropdown
'
;
import
GfmAutoComplete
from
'
./gfm_auto_complete
'
;
const
ShortcutsBlob
=
require
(
'
./shortcuts_blob
'
);
...
...
@@ -79,6 +80,8 @@ const ShortcutsBlob = require('./shortcuts_blob');
path
=
page
.
split
(
'
:
'
);
shortcut_handler
=
null
;
new
GfmAutoComplete
(
gl
.
GfmAutoComplete
&&
gl
.
GfmAutoComplete
.
dataSources
).
setup
();
function
initBlob
()
{
new
LineHighlighter
();
...
...
app/assets/javascripts/gfm_auto_complete.js
View file @
b8b6de4c
/* 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
'
;
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
,
enableMap
=
{
setup
(
input
,
enableMap
=
{
emojis
:
true
,
members
:
true
,
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
'
);
this
.
enableMap
=
enableMap
;
this
.
setupLifecycle
();
},
}
setupLifecycle
()
{
this
.
input
.
each
((
i
,
input
)
=>
{
const
$input
=
$
(
input
);
...
...
@@ -122,9 +36,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
);
...
...
@@ -138,10 +52,11 @@ window.gl.GfmAutoComplete = {
alias
:
'
commands
'
,
searchKey
:
'
search
'
,
skipSpecialCharacterTest
:
true
,
data
:
this
.
defaultLoadingData
,
displayTpl
:
function
(
value
)
{
if
(
this
.
isLoading
(
value
))
return
this
.
Loading
.
template
;
var
tpl
=
'
<li>/${name}
'
;
data
:
GfmAutoComplete
.
defaultLoadingData
,
displayTpl
(
value
)
{
if
(
GfmAutoComplete
.
isLoading
(
value
))
return
GfmAutoComplete
.
Loading
.
template
;
// eslint-disable-next-line no-template-curly-in-string
let
tpl
=
'
<li>/${name}
'
;
if
(
value
.
aliases
.
length
>
0
)
{
tpl
+=
'
<small>(or /<%- aliases.join(", /") %>)</small>
'
;
}
...
...
@@ -153,105 +68,106 @@ window.gl.GfmAutoComplete = {
}
tpl
+=
'
</li>
'
;
return
_
.
template
(
tpl
)(
value
);
}.
bind
(
this
),
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
_p
refix
=
value
.
params
[
0
][
0
];
if
(
/^
[
@%~
]
/
.
test
(
reference
_p
refix
))
{
tpl
+=
'
<%- reference
_p
refix %>
'
;
reference
P
refix
=
value
.
params
[
0
][
0
];
if
(
/^
[
@%~
]
/
.
test
(
reference
P
refix
))
{
tpl
+=
'
<%- reference
P
refix %>
'
;
}
}
return
_
.
template
(
tpl
)({
reference
_prefix
:
reference_p
refix
});
return
_
.
template
(
tpl
)({
reference
P
refix
});
},
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
;
...
this
.
getDefaultCallbacks
(),
beforeSave
(
commands
)
{
if
(
GfmAutoComplete
.
isLoading
(
commands
))
return
commands
;
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
;
},
},
});
return
;
},
}
setupEmoji
(
$input
)
{
// Emoji
$input
.
atwho
({
at
:
'
:
'
,
displayTpl
:
function
(
value
)
{
return
value
&&
value
.
name
?
this
.
Emoji
.
templateFunction
(
value
.
name
)
:
this
.
Loading
.
template
;
}.
bind
(
this
),
displayTpl
(
value
)
{
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
:
this
.
defaultLoadingData
,
data
:
GfmAutoComplete
.
defaultLoadingData
,
callbacks
:
{
sorter
:
this
.
DefaultOptions
.
sorter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
filter
:
this
.
DefaultOptions
.
filter
,
matcher
:
(
flag
,
subtext
)
=>
{
...
this
.
getDefaultCallbacks
(),
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
)
{
// Team Members
$input
.
atwho
({
at
:
'
@
'
,
displayTpl
:
function
(
value
)
{
return
value
.
username
!=
null
?
this
.
Members
.
template
:
this
.
Loading
.
template
;
}.
bind
(
this
),
displayTpl
(
value
)
{
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
,
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
,
beforeSave
:
function
(
members
)
{
return
$
.
map
(
members
,
function
(
m
)
{
...
this
.
getDefaultCallbacks
(),
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
();
...
...
@@ -262,173 +178,271 @@ window.gl.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
}
`
),
};
});
}
}
}
,
}
,
});
}
,
}
setupIssues
(
$input
)
{
$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
)
{
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
:
this
.
DefaultOptions
.
sorter
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
matcher
:
this
.
DefaultOptions
.
matcher
,
beforeSave
:
function
(
issues
)
{
return
$
.
map
(
issues
,
function
(
i
)
{
...
this
.
getDefaultCallbacks
(),
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
}
`
,
};
});
}
}
}
,
}
,
});
}
,
}
setupMilestones
(
$input
)
{
$input
.
atwho
({
at
:
'
%
'
,
alias
:
'
milestones
'
,
searchKey
:
'
search
'
,
// eslint-disable-next-line no-template-curly-in-string
insertTpl
:
'
${atwho-at}${title}
'
,
displayTpl
:
function
(
value
)
{
return
value
.
title
!=
null
?
this
.
Milestones
.
template
:
this
.
Loading
.
template
;
}.
bind
(
this
),
data
:
this
.
defaultLoadingData
,
displayTpl
(
value
)
{
let
tmpl
=
GfmAutoComplete
.
Loading
.
template
;
if
(
value
.
title
!=
null
)
{
tmpl
=
GfmAutoComplete
.
Milestones
.
template
;
}
return
tmpl
;
},
data
:
GfmAutoComplete
.
defaultLoadingData
,
callbacks
:
{
matcher
:
this
.
DefaultOptions
.
matcher
,
sorter
:
this
.
DefaultOptions
.
sorter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeSave
:
function
(
milestones
)
{
return
$
.
map
(
milestones
,
function
(
m
)
{
...
this
.
getDefaultCallbacks
(),
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
,
};
});
}
}
}
,
}
,
});
}
,
}
setupMergeRequests
(
$input
)
{
$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
)
{
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
:
this
.
DefaultOptions
.
sorter
,
filter
:
this
.
DefaultOptions
.
filter
,
beforeInsert
:
this
.
DefaultOptions
.
beforeInsert
,
matcher
:
this
.
DefaultOptions
.
matcher
,
beforeSave
:
function
(
merges
)
{
return
$
.
map
(
merges
,
function
(
m
)
{
...
this
.
getDefaultCallbacks
(),
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
}
`
,
};
});
}
}
}
,
}
,
});
}
,
}
setupLabels
(
$input
)
{
$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
)
{
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
:
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
;
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
};
});
}
}
...
this
.
getDefaultCallbacks
(),
beforeSave
(
merges
)
{
if
(
GfmAutoComplete
.
isLoading
(
merges
))
return
merges
;
return
$
.
map
(
merges
,
m
=>
({
title
:
sanitize
(
m
.
title
),
color
:
m
.
color
,
search
:
m
.
title
,
}));
},
},
});
}
,
}
fetchData
:
function
(
$input
,
at
)
{
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
;
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
)
{
var
dataToInspect
=
data
;
}
static
isLoading
(
data
)
{
let
dataToInspect
=
data
;
if
(
data
&&
data
.
length
>
0
)
{
dataToInspect
=
data
[
0
];
}
var
loadingState
=
this
.
defaultLoadingData
[
0
];
const
loadingState
=
GfmAutoComplete
.
defaultLoadingData
[
0
];
return
dataToInspect
&&
(
dataToInspect
===
loadingState
||
dataToInspect
.
name
===
loadingState
);
}
}
GfmAutoComplete
.
defaultLoadingData
=
[
'
loading
'
];
GfmAutoComplete
.
atTypeMap
=
{
'
:
'
:
'
emojis
'
,
'
@
'
:
'
members
'
,
'
#
'
:
'
issues
'
,
'
!
'
:
'
mergeRequests
'
,
'
~
'
:
'
labels
'
,
'
%
'
:
'
milestones
'
,
'
/
'
:
'
commands
'
,
};
// Emoji
GfmAutoComplete
.
Emoji
=
{
templateFunction
(
name
)
{
return
`<li>
${
name
}
${
glEmojiTag
(
name
)}
</li>
`
;
},
};
// Team Members
GfmAutoComplete
.
Members
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li>${avatarTag} ${username} <small>${title}</small></li>
'
,
};
GfmAutoComplete
.
Labels
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>
'
,
};
// Issues and MergeRequests
GfmAutoComplete
.
Issues
=
{
// eslint-disable-next-line no-template-curly-in-string
template
:
'
<li><small>${id}</small> ${title}</li>
'
,
};
// Milestones
GfmAutoComplete
.
Milestones
=
{
// eslint-disable-next-line no-template-curly-in-string
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
;
app/assets/javascripts/gl_form.js
View file @
b8b6de4c
...
...
@@ -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
);
}
...
...
app/assets/javascripts/issuable_form.js
View file @
b8b6de4c
...
...
@@ -6,6 +6,7 @@
/* global Pikaday */
import
UsersSelect
from
'
./users_select
'
;
import
GfmAutoComplete
from
'
./gfm_auto_complete
'
;
(
function
()
{
this
.
IssuableForm
=
(
function
()
{
...
...
@@ -20,7 +21,7 @@ import UsersSelect from './users_select';
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
ZenMode
();
this
.
titleField
=
this
.
form
.
find
(
"
input[name*='[title]']
"
);
...
...
app/assets/javascripts/main.js
View file @
b8b6de4c
...
...
@@ -96,7 +96,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
'
;
...
...
app/assets/javascripts/notes.js
View file @
b8b6de4c
...
...
@@ -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
'
);
...
...
app/views/layouts/_init_auto_complete.html.haml
View file @
b8b6de4c
...
...
@@ -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
();
app/views/layouts/application.html.haml
View file @
b8b6de4c
...
...
@@ -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
spec/features/projects/gfm_autocomplete_load_spec.rb
View file @
b8b6de4c
...
...
@@ -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
...
...
spec/javascripts/gfm_auto_complete_spec.js
View file @
b8b6de4c
/* 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
()
{
const
gfmAutoCompleteCallbacks
=
GfmAutoComplete
.
prototype
.
getDefaultCallbacks
.
call
({
fetchData
:
()
=>
{},
});
describe
(
'
DefaultOptions.sorter
'
,
function
()
{
describe
(
'
assets loading
'
,
function
()
{
beforeEach
(
function
()
{
...
...
@@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () {
this
.
atwhoInstance
=
{
setting
:
{}
};
this
.
items
=
[];
this
.
sorterValue
=
GfmAutoComplete
.
DefaultOption
s
.
sorter
this
.
sorterValue
=
gfmAutoCompleteCallback
s
.
sorter
.
call
(
this
.
atwhoInstance
,
''
,
this
.
items
);
});
...
...
@@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () {
it
(
'
should enable highlightFirst if alwaysHighlightFirst is set
'
,
function
()
{
const
atwhoInstance
=
{
setting
:
{
alwaysHighlightFirst
:
true
}
};
GfmAutoComplete
.
DefaultOption
s
.
sorter
.
call
(
atwhoInstance
);
gfmAutoCompleteCallback
s
.
sorter
.
call
(
atwhoInstance
);
expect
(
atwhoInstance
.
setting
.
highlightFirst
).
toBe
(
true
);
});
...
...
@@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () {
it
(
'
should enable highlightFirst if a query is present
'
,
function
()
{
const
atwhoInstance
=
{
setting
:
{}
};
GfmAutoComplete
.
DefaultOption
s
.
sorter
.
call
(
atwhoInstance
,
'
query
'
);
gfmAutoCompleteCallback
s
.
sorter
.
call
(
atwhoInstance
,
'
query
'
);
expect
(
atwhoInstance
.
setting
.
highlightFirst
).
toBe
(
true
);
});
...
...
@@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () {
const
items
=
[];
const
searchKey
=
'
searchKey
'
;
GfmAutoComplete
.
DefaultOption
s
.
sorter
.
call
(
atwhoInstance
,
query
,
items
,
searchKey
);
gfmAutoCompleteCallback
s
.
sorter
.
call
(
atwhoInstance
,
query
,
items
,
searchKey
);
expect
(
$
.
fn
.
atwho
.
default
.
callbacks
.
sorter
).
toHaveBeenCalledWith
(
query
,
items
,
searchKey
);
});
...
...
@@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () {
describe
(
'
DefaultOptions.matcher
'
,
function
()
{
const
defaultMatcher
=
(
context
,
flag
,
subtext
)
=>
(
GfmAutoComplete
.
DefaultOption
s
.
matcher
.
call
(
context
,
flag
,
subtext
)
gfmAutoCompleteCallback
s
.
matcher
.
call
(
context
,
flag
,
subtext
)
);
const
flagsUseDefaultMatcher
=
[
'
@
'
,
'
#
'
,
'
!
'
,
'
~
'
,
'
%
'
];
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment