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
0
Merge Requests
0
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
Léo-Paul Géneau
gitlab-ce
Commits
dcf09a53
Commit
dcf09a53
authored
Jul 27, 2016
by
Luke Bennett
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Moved changes across to es5 and changed spec to es6
parent
e74d12a9
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
175 additions
and
1131 deletions
+175
-1131
app/assets/javascripts/gl_dropdown.js
app/assets/javascripts/gl_dropdown.js
+41
-24
app/assets/javascripts/gl_dropdown.js.coffee
app/assets/javascripts/gl_dropdown.js.coffee
+0
-658
app/assets/javascripts/search_autocomplete.js
app/assets/javascripts/search_autocomplete.js
+14
-4
app/assets/javascripts/search_autocomplete.js.coffee
app/assets/javascripts/search_autocomplete.js.coffee
+0
-348
spec/javascripts/gl_dropdown_spec.js.coffee
spec/javascripts/gl_dropdown_spec.js.coffee
+0
-97
spec/javascripts/gl_dropdown_spec.js.es6
spec/javascripts/gl_dropdown_spec.js.es6
+120
-0
No files found.
app/assets/javascripts/gl_dropdown.js
View file @
dcf09a53
...
...
@@ -191,6 +191,12 @@
currentIndex
=
-
1
;
NON_SELECTABLE_CLASSES
=
'
.divider, .separator, .dropdown-header, .dropdown-menu-empty-link
'
;
SELECTABLE_CLASSES
=
"
.dropdown-content li:not(
"
+
NON_SELECTABLE_CLASSES
+
"
)
"
;
CURSOR_SELECT_SCROLL_PADDING
=
5
FILTER_INPUT
=
'
.dropdown-input .dropdown-input-field
'
;
function
GitLabDropdown
(
el1
,
options
)
{
...
...
@@ -213,6 +219,7 @@
if
(
this
.
options
.
data
)
{
if
(
_
.
isObject
(
this
.
options
.
data
)
&&
!
_
.
isFunction
(
this
.
options
.
data
))
{
this
.
fullData
=
this
.
options
.
data
;
currentIndex
=
-
1
this
.
parseData
(
this
.
options
.
data
);
}
else
{
this
.
remote
=
new
GitLabDropdownRemote
(
this
.
options
.
data
,
{
...
...
@@ -240,7 +247,7 @@
keys
:
searchFields
,
elements
:
(
function
(
_this
)
{
return
function
()
{
selector
=
'
.dropdown-content li:not(.divider)
'
;
selector
=
SELECTABLE_CLASSES
;
if
(
_this
.
dropdown
.
find
(
'
.dropdown-toggle-page
'
).
length
)
{
selector
=
"
.dropdown-page-one
"
+
selector
;
}
...
...
@@ -376,7 +383,7 @@
var
$target
;
if
(
this
.
options
.
multiSelect
)
{
$target
=
$
(
e
.
target
);
if
(
!
$target
.
hasClass
(
'
dropdown-menu-close
'
)
&&
!
$target
.
hasClass
(
'
dropdown-menu-close-icon
'
)
&&
!
$target
.
data
(
'
is-link
'
))
{
if
(
$target
&&
!
$target
.
hasClass
(
'
dropdown-menu-close
'
)
&&
!
$target
.
hasClass
(
'
dropdown-menu-close-icon
'
)
&&
!
$target
.
data
(
'
is-link
'
))
{
e
.
stopPropagation
();
return
false
;
}
else
{
...
...
@@ -387,7 +394,7 @@
GitLabDropdown
.
prototype
.
opened
=
function
()
{
var
contentHtml
;
currentIndex
=
-
1
;
this
.
resetRows
()
;
this
.
addArrowKeyEvent
();
if
(
this
.
options
.
setIndeterminateIds
)
{
this
.
options
.
setIndeterminateIds
.
call
(
this
);
...
...
@@ -410,6 +417,7 @@
GitLabDropdown
.
prototype
.
hidden
=
function
(
e
)
{
var
$input
;
this
.
resetRows
();
this
.
removeArrayKeyEvent
();
$input
=
this
.
dropdown
.
find
(
"
.dropdown-input-field
"
);
if
(
this
.
options
.
filterable
)
{
...
...
@@ -463,7 +471,7 @@
return
"
<li class='separator'></li>
"
;
}
if
(
data
.
header
!=
null
)
{
return
"
<li class='dropdown-header'>
"
+
data
.
header
+
"
</li>
"
;
return
_
.
template
(
'
<li class="dropdown-header"><%- header %></li>
'
)({
header
:
data
.
header
})
;
}
if
(
this
.
options
.
renderRow
)
{
html
=
this
.
options
.
renderRow
.
call
(
this
.
options
,
data
,
this
);
...
...
@@ -498,7 +506,12 @@
}
else
{
groupAttrs
=
''
;
}
html
=
"
<li> <a href='
"
+
url
+
"
'
"
+
groupAttrs
+
"
class='
"
+
cssClass
+
"
'>
"
+
text
+
"
</a> </li>
"
;
html
=
_
.
template
(
'
<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>
'
)({
url
:
url
,
groupAttrs
:
groupAttrs
,
cssClass
:
cssClass
,
text
:
text
});
}
return
html
;
};
...
...
@@ -520,17 +533,6 @@
return
html
=
"
<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>
"
;
};
GitLabDropdown
.
prototype
.
highlightRow
=
function
(
index
)
{
var
selector
;
if
(
this
.
filterInput
.
val
()
!==
""
)
{
selector
=
'
.dropdown-content li:first-child a
'
;
if
(
this
.
dropdown
.
find
(
"
.dropdown-toggle-page
"
).
length
)
{
selector
=
"
.dropdown-page-one .dropdown-content li:first-child a
"
;
}
return
this
.
getElement
(
selector
).
addClass
(
'
is-focused
'
);
}
};
GitLabDropdown
.
prototype
.
rowClicked
=
function
(
el
)
{
var
field
,
fieldName
,
groupName
,
isInput
,
selectedIndex
,
selectedObject
,
value
;
fieldName
=
this
.
options
.
fieldName
;
...
...
@@ -609,13 +611,17 @@
GitLabDropdown
.
prototype
.
selectRowAtIndex
=
function
(
index
)
{
var
$el
,
selector
;
selector
=
"
.dropdown-content li:not(.divider,.dropdown-header,.separator)
:eq(
"
+
index
+
"
) a
"
;
selector
=
SELECTABLE_CLASSES
+
"
:eq(
"
+
index
+
"
) a
"
;
if
(
this
.
dropdown
.
find
(
"
.dropdown-toggle-page
"
).
length
)
{
selector
=
"
.dropdown-page-one
"
+
selector
;
}
$el
=
$
(
selector
,
this
.
dropdown
);
if
(
$el
.
length
)
{
return
$el
.
first
().
trigger
(
'
click
'
);
e
.
preventDefault
();
e
.
stopImmediatePropagation
();
$el
.
first
().
trigger
(
'
click
'
);
href
=
$el
.
attr
(
'
href
'
);
if
(
href
&&
href
!==
'
#
'
)
Turbolinks
.
visit
(
href
);
}
};
...
...
@@ -623,7 +629,7 @@
var
$input
,
ARROW_KEY_CODES
,
selector
;
ARROW_KEY_CODES
=
[
38
,
40
];
$input
=
this
.
dropdown
.
find
(
"
.dropdown-input-field
"
);
selector
=
'
.dropdown-content li:not(.divider,.dropdown-header,.separator):visible
'
;
selector
=
SELECTABLE_CLASSES
;
if
(
this
.
dropdown
.
find
(
"
.dropdown-toggle-page
"
).
length
)
{
selector
=
"
.dropdown-page-one
"
+
selector
;
}
...
...
@@ -651,7 +657,9 @@
return
false
;
}
if
(
currentKeyCode
===
13
&&
currentIndex
!==
-
1
)
{
return
_this
.
selectRowAtIndex
(
$
(
'
.is-focused
'
,
_this
.
dropdown
).
closest
(
'
li
'
).
index
()
-
1
);
e
.
preventDefault
()
e
.
stopImmediatePropagation
()
return
_this
.
selectRowAtIndex
(
currentIndex
);
}
};
})(
this
));
...
...
@@ -661,6 +669,11 @@
return
$
(
'
body
'
).
off
(
'
keydown
'
);
};
GitLabDropdown
.
prototype
.
resetRows
=
function
resetRows
()
{
currentIndex
=
-
1
;
$
(
'
.is-focused
'
,
this
.
dropdown
).
removeClass
(
'
is-focused
'
);
};
GitLabDropdown
.
prototype
.
highlightRowAtIndex
=
function
(
$listItems
,
index
)
{
var
$dropdownContent
,
$listItem
,
dropdownContentBottom
,
dropdownContentHeight
,
dropdownContentTop
,
dropdownScrollTop
,
listItemBottom
,
listItemHeight
,
listItemTop
;
$
(
'
.is-focused
'
,
this
.
dropdown
).
removeClass
(
'
is-focused
'
);
...
...
@@ -674,10 +687,14 @@
listItemHeight
=
$listItem
.
outerHeight
();
listItemTop
=
$listItem
.
prop
(
'
offsetTop
'
);
listItemBottom
=
listItemTop
+
listItemHeight
;
if
(
listItemBottom
>
dropdownContentBottom
+
dropdownScrollTop
)
{
return
$dropdownContent
.
scrollTop
(
listItemBottom
-
dropdownContentBottom
);
}
else
if
(
listItemTop
<
dropdownContentTop
+
dropdownScrollTop
)
{
return
$dropdownContent
.
scrollTop
(
listItemTop
-
dropdownContentTop
);
if
(
!
index
)
{
$dropdownContent
.
scrollTop
(
0
)
}
else
if
(
index
===
(
$listItems
.
length
-
1
))
{
$dropdownContent
.
scrollTop
$dropdownContent
.
prop
(
'
scrollHeight
'
);
}
else
if
(
listItemBottom
>
(
dropdownContentBottom
+
dropdownScrollTop
))
$dropdownContent
.
scrollTop
(
listItemBottom
-
dropdownContentBottom
+
CURSOR_SELECT_SCROLL_PADDING
);
}
else
if
(
listItemTop
<
(
dropdownContentTop
+
dropdownScrollTop
))
{
return
$dropdownContent
.
scrollTop
(
listItemTop
-
dropdownContentTop
-
CURSOR_SELECT_SCROLL_PADDING
);
}
};
...
...
app/assets/javascripts/gl_dropdown.js.coffee
deleted
100644 → 0
View file @
e74d12a9
class
GitLabDropdownFilter
BLUR_KEYCODES
=
[
27
,
40
]
ARROW_KEY_CODES
=
[
38
,
40
]
HAS_VALUE_CLASS
=
"has-value"
constructor
:
(
@
input
,
@
options
)
->
{
@
filterInputBlur
=
true
}
=
@
options
$inputContainer
=
@
input
.
parent
()
$clearButton
=
$inputContainer
.
find
(
'.js-dropdown-input-clear'
)
@
indeterminateIds
=
[]
# Clear click
$clearButton
.
on
'click'
,
(
e
)
=>
e
.
preventDefault
()
e
.
stopPropagation
()
@
input
.
val
(
''
)
.
trigger
(
'keyup'
)
.
focus
()
# Key events
timeout
=
""
@
input
.
on
"keyup"
,
(
e
)
=>
keyCode
=
e
.
which
return
if
ARROW_KEY_CODES
.
indexOf
(
keyCode
)
>=
0
if
@
input
.
val
()
isnt
""
and
!
$inputContainer
.
hasClass
HAS_VALUE_CLASS
$inputContainer
.
addClass
HAS_VALUE_CLASS
else
if
@
input
.
val
()
is
""
and
$inputContainer
.
hasClass
HAS_VALUE_CLASS
$inputContainer
.
removeClass
HAS_VALUE_CLASS
if
keyCode
is
13
return
false
# Only filter asynchronously only if option remote is set
if
@
options
.
remote
clearTimeout
timeout
timeout
=
setTimeout
=>
blur_field
=
@
shouldBlur
keyCode
if
blur_field
and
@
filterInputBlur
@
input
.
blur
()
@
options
.
query
@
input
.
val
(),
(
data
)
=>
@
options
.
callback
(
data
)
,
250
else
@
filter
@
input
.
val
()
shouldBlur
:
(
keyCode
)
->
return
BLUR_KEYCODES
.
indexOf
(
keyCode
)
>=
0
filter
:
(
search_text
)
->
@
options
.
onFilter
(
search_text
)
if
@
options
.
onFilter
data
=
@
options
.
data
()
if
data
?
and
not
@
options
.
filterByText
results
=
data
if
search_text
isnt
''
# When data is an array of objects therefore [object Array] e.g.
# [
# { prop: 'foo' },
# { prop: 'baz' }
# ]
if
_
.
isArray
(
data
)
results
=
fuzzaldrinPlus
.
filter
(
data
,
search_text
,
key
:
@
options
.
keys
)
else
# If data is grouped therefore an [object Object]. e.g.
# {
# groupName1: [
# { prop: 'foo' },
# { prop: 'baz' }
# ],
# groupName2: [
# { prop: 'abc' },
# { prop: 'def' }
# ]
# }
if
gl
.
utils
.
isObject
data
results
=
{}
for
key
,
group
of
data
tmp
=
fuzzaldrinPlus
.
filter
(
group
,
search_text
,
key
:
@
options
.
keys
)
if
tmp
.
length
results
[
key
]
=
tmp
.
map
(
item
)
->
item
@
options
.
callback
results
else
elements
=
@
options
.
elements
()
if
search_text
elements
.
each
->
$el
=
$
(
@
)
matches
=
fuzzaldrinPlus
.
match
(
$el
.
text
().
trim
(),
search_text
)
unless
$el
.
is
(
'.dropdown-header'
)
if
matches
.
length
$el
.
show
()
else
$el
.
hide
()
else
elements
.
show
()
class
GitLabDropdownRemote
constructor
:
(
@
dataEndpoint
,
@
options
)
->
execute
:
->
if
typeof
@
dataEndpoint
is
"string"
@
fetchData
()
else
if
typeof
@
dataEndpoint
is
"function"
if
@
options
.
beforeSend
@
options
.
beforeSend
()
# Fetch the data by calling the data funcfion
@
dataEndpoint
""
,
(
data
)
=>
if
@
options
.
success
@
options
.
success
(
data
)
if
@
options
.
beforeSend
@
options
.
beforeSend
()
# Fetch the data through ajax if the data is a string
fetchData
:
->
$
.
ajax
(
url
:
@
dataEndpoint
,
dataType
:
@
options
.
dataType
,
beforeSend
:
=>
if
@
options
.
beforeSend
@
options
.
beforeSend
()
success
:
(
data
)
=>
if
@
options
.
success
@
options
.
success
(
data
)
)
class
GitLabDropdown
LOADING_CLASS
=
"is-loading"
PAGE_TWO_CLASS
=
"is-page-two"
ACTIVE_CLASS
=
"is-active"
INDETERMINATE_CLASS
=
"is-indeterminate"
currentIndex
=
-
1
NON_SELECTABLE_CLASSES
=
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'
SELECTABLE_CLASSES
=
".dropdown-content li:not(
#{
NON_SELECTABLE_CLASSES
}
)"
FILTER_INPUT
=
'.dropdown-input .dropdown-input-field'
CURSOR_SELECT_SCROLL_PADDING
=
5
constructor
:
(
@
el
,
@
options
)
->
self
=
@
selector
=
$
(
@
el
).
data
"target"
@
dropdown
=
if
selector
?
then
$
(
selector
)
else
$
(
@
el
).
parent
()
# Set Defaults
{
# If no input is passed create a default one
@
filterInput
=
@
getElement
(
FILTER_INPUT
)
@
highlight
=
false
@
filterInputBlur
=
true
}
=
@
options
self
=
@
# If selector was passed
if
_
.
isString
(
@
filterInput
)
@
filterInput
=
@
getElement
(
@
filterInput
)
searchFields
=
if
@
options
.
search
then
@
options
.
search
.
fields
else
[]
if
@
options
.
data
# If we provided data
# data could be an array of objects or a group of arrays
if
_
.
isObject
(
@
options
.
data
)
and
not
_
.
isFunction
(
@
options
.
data
)
@
fullData
=
@
options
.
data
@
parseData
@
options
.
data
else
# Remote data
@
remote
=
new
GitLabDropdownRemote
@
options
.
data
,
{
dataType
:
@
options
.
dataType
,
beforeSend
:
@
toggleLoading
.
bind
(
@
)
success
:
(
data
)
=>
@
fullData
=
data
# Reset selected row index on new data
currentIndex
=
-
1
@
parseData
@
fullData
@
filter
.
input
.
trigger
(
'keyup'
)
if
@
options
.
filterable
and
@
filter
and
@
filter
.
input
}
# Init filterable
if
@
options
.
filterable
@
filter
=
new
GitLabDropdownFilter
@
filterInput
,
filterInputBlur
:
@
filterInputBlur
filterByText
:
@
options
.
filterByText
onFilter
:
@
options
.
onFilter
remote
:
@
options
.
filterRemote
query
:
@
options
.
data
keys
:
searchFields
elements
:
=>
selector
=
SELECTABLE_CLASSES
if
@
dropdown
.
find
(
'.dropdown-toggle-page'
).
length
selector
=
".dropdown-page-one
#{
selector
}
"
return
$
(
selector
)
data
:
=>
return
@
fullData
callback
:
(
data
)
=>
@
parseData
data
unless
@
filterInput
.
val
()
is
''
selector
=
'.dropdown-content li:not(.divider):visible'
if
@
dropdown
.
find
(
'.dropdown-toggle-page'
).
length
selector
=
".dropdown-page-one
#{
selector
}
"
$
(
selector
,
@
dropdown
)
.
first
()
.
find
(
'a'
)
.
addClass
(
'is-focused'
)
currentIndex
=
0
# Event listeners
@
dropdown
.
on
"shown.bs.dropdown"
,
@
opened
@
dropdown
.
on
"hidden.bs.dropdown"
,
@
hidden
$
(
@
el
).
on
"update.label"
,
@
updateLabel
@
dropdown
.
on
"click"
,
".dropdown-menu, .dropdown-menu-close"
,
@
shouldPropagate
@
dropdown
.
on
'keyup'
,
(
e
)
=>
if
e
.
which
is
27
# Escape key
$
(
'.dropdown-menu-close'
,
@
dropdown
).
trigger
'click'
@
dropdown
.
on
'blur'
,
'a'
,
(
e
)
=>
if
e
.
relatedTarget
?
$relatedTarget
=
$
(
e
.
relatedTarget
)
$dropdownMenu
=
$relatedTarget
.
closest
(
'.dropdown-menu'
)
if
$dropdownMenu
.
length
is
0
@
dropdown
.
removeClass
(
'open'
)
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
@
dropdown
.
find
(
".dropdown-toggle-page, .dropdown-menu-back"
).
on
"click"
,
(
e
)
=>
e
.
preventDefault
()
e
.
stopPropagation
()
@
togglePage
()
if
@
options
.
selectable
selector
=
".dropdown-content a"
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
selector
=
".dropdown-page-one .dropdown-content a"
@
dropdown
.
on
"click"
,
selector
,
(
e
)
->
$el
=
$
(
@
)
selected
=
self
.
rowClicked
$el
if
self
.
options
.
clicked
self
.
options
.
clicked
(
selected
,
$el
,
e
)
$el
.
trigger
(
'blur'
)
# Finds an element inside wrapper element
getElement
:
(
selector
)
->
@
dropdown
.
find
selector
toggleLoading
:
->
$
(
'.dropdown-menu'
,
@
dropdown
).
toggleClass
LOADING_CLASS
togglePage
:
->
menu
=
$
(
'.dropdown-menu'
,
@
dropdown
)
if
menu
.
hasClass
(
PAGE_TWO_CLASS
)
if
@
remote
@
remote
.
execute
()
menu
.
toggleClass
PAGE_TWO_CLASS
# Focus first visible input on active page
@
dropdown
.
find
(
'[class^="dropdown-page-"]:visible :text:visible:first'
).
focus
()
parseData
:
(
data
)
->
@
renderedData
=
data
if
@
options
.
filterable
and
data
.
length
is
0
# render no matching results
html
=
[
@
noResults
()]
else
# Handle array groups
if
gl
.
utils
.
isObject
data
html
=
[]
for
name
,
groupData
of
data
# Add header for each group
html
.
push
(
@
renderItem
(
header
:
name
,
name
))
@
renderData
(
groupData
,
name
)
.
map
(
item
)
->
html
.
push
item
else
# Render each row
html
=
@
renderData
(
data
)
# Render the full menu
full_html
=
@
renderMenu
(
html
)
@
appendMenu
(
full_html
)
renderData
:
(
data
,
group
=
false
)
->
data
.
map
(
obj
,
index
)
=>
return
@
renderItem
(
obj
,
group
,
index
)
shouldPropagate
:
(
e
)
=>
if
@
options
.
multiSelect
$target
=
$
(
e
.
target
)
if
$target
and
not
$target
.
hasClass
(
'dropdown-menu-close'
)
and
not
$target
.
hasClass
(
'dropdown-menu-close-icon'
)
and
not
$target
.
data
(
'is-link'
)
e
.
stopPropagation
()
return
false
else
return
true
opened
:
=>
@
resetRows
()
@
addArrowKeyEvent
()
if
@
options
.
setIndeterminateIds
@
options
.
setIndeterminateIds
.
call
(
@
)
if
@
options
.
setActiveIds
@
options
.
setActiveIds
.
call
(
@
)
# Makes indeterminate items effective
if
@
fullData
and
@
dropdown
.
find
(
'.dropdown-menu-toggle'
).
hasClass
(
'js-filter-bulk-update'
)
@
parseData
@
fullData
contentHtml
=
$
(
'.dropdown-content'
,
@
dropdown
).
html
()
if
@
remote
&&
contentHtml
is
""
@
remote
.
execute
()
if
@
options
.
filterable
@
filterInput
.
focus
()
@
dropdown
.
trigger
(
'shown.gl.dropdown'
)
hidden
:
(
e
)
=>
@
resetRows
()
@
removeArrayKeyEvent
()
$input
=
@
dropdown
.
find
(
".dropdown-input-field"
)
if
@
options
.
filterable
$input
.
blur
()
.
val
(
""
)
# Triggering 'keyup' will re-render the dropdown which is not always required
# specially if we want to keep the state of the dropdown needed for bulk-assignment
if
not
@
options
.
persistWhenHide
$input
.
trigger
(
"keyup"
)
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
$
(
'.dropdown-menu'
,
@
dropdown
).
removeClass
PAGE_TWO_CLASS
if
@
options
.
hidden
@
options
.
hidden
.
call
(
@
,
e
)
@
dropdown
.
trigger
(
'hidden.gl.dropdown'
)
# Render the full menu
renderMenu
:
(
html
)
->
menu_html
=
""
if
@
options
.
renderMenu
menu_html
=
@
options
.
renderMenu
(
html
)
else
menu_html
=
$
(
'<ul />'
)
.
append
(
html
)
return
menu_html
# Append the menu into the dropdown
appendMenu
:
(
html
)
->
selector
=
'.dropdown-content'
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
selector
=
".dropdown-page-one .dropdown-content"
$
(
selector
,
@
dropdown
)
.
empty
()
.
append
(
html
)
# Render the row
renderItem
:
(
data
,
group
=
false
,
index
=
false
)
->
html
=
""
# Divider
return
'<li class="divider"></li>'
if
data
is
'divider'
# Separator is a full-width divider
return
'<li class="separator"></li>'
if
data
is
'separator'
# Header
return
_
.
template
(
'<li class="dropdown-header"><%- header %></li>'
)({
header
:
data
.
header
})
if
data
.
header
?
if
@
options
.
renderRow
# Call the render function
html
=
@
options
.
renderRow
.
call
(
@
options
,
data
,
@
)
else
if
not
selected
value
=
if
@
options
.
id
then
@
options
.
id
(
data
)
else
data
.
id
fieldName
=
@
options
.
fieldName
field
=
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
'][value='
#{
value
}
']"
)
if
field
.
length
selected
=
true
# Set URL
if
@
options
.
url
?
url
=
@
options
.
url
(
data
)
else
url
=
if
data
.
url
?
then
data
.
url
else
'#'
# Set Text
if
@
options
.
text
?
text
=
@
options
.
text
(
data
)
else
text
=
if
data
.
text
?
then
data
.
text
else
''
cssClass
=
""
if
selected
cssClass
=
"is-active"
if
@
highlight
text
=
@
highlightTextMatches
(
text
,
@
filterInput
.
val
())
if
group
groupAttrs
=
"data-group=
#{
group
}
data-index=
#{
index
}
"
else
groupAttrs
=
''
html
=
_
.
template
(
'<li>
<a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>">
<%= text %>
</a>
</li>'
)({
url
:
url
groupAttrs
:
groupAttrs
cssClass
:
cssClass
text
:
text
})
return
html
highlightTextMatches
:
(
text
,
term
)
->
occurrences
=
fuzzaldrinPlus
.
match
(
text
,
term
)
text
.
split
(
''
).
map
((
character
,
i
)
->
if
i
in
occurrences
then
"<b>
#{
character
}
</b>"
else
character
).
join
(
''
)
noResults
:
->
html
=
'<li class="dropdown-menu-empty-link">
<a href="#" class="is-focused">
No matching results.
</a>
</li>'
rowClicked
:
(
el
)
->
fieldName
=
@
options
.
fieldName
isInput
=
$
(
@
el
).
is
(
'input'
)
if
@
renderedData
groupName
=
el
.
data
(
'group'
)
if
groupName
selectedIndex
=
el
.
data
(
'index'
)
selectedObject
=
@
renderedData
[
groupName
][
selectedIndex
]
else
selectedIndex
=
el
.
closest
(
'li'
).
index
()
selectedObject
=
@
renderedData
[
selectedIndex
]
value
=
if
@
options
.
id
then
@
options
.
id
(
selectedObject
,
el
)
else
selectedObject
.
id
if
isInput
field
=
$
(
@
el
)
else
field
=
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
'][value='
#{
value
}
']"
)
if
el
.
hasClass
(
ACTIVE_CLASS
)
el
.
removeClass
(
ACTIVE_CLASS
)
if
isInput
field
.
val
(
''
)
else
field
.
remove
()
# Toggle the dropdown label
if
@
options
.
toggleLabel
@
updateLabel
(
selectedObject
,
el
,
@
)
else
selectedObject
else
if
el
.
hasClass
(
INDETERMINATE_CLASS
)
el
.
addClass
ACTIVE_CLASS
el
.
removeClass
INDETERMINATE_CLASS
if
not
value
?
field
.
remove
()
if
not
field
.
length
and
fieldName
@
addInput
(
fieldName
,
value
)
return
selectedObject
else
if
not
@
options
.
multiSelect
or
el
.
hasClass
(
'dropdown-clear-active'
)
@
dropdown
.
find
(
".
#{
ACTIVE_CLASS
}
"
).
removeClass
ACTIVE_CLASS
unless
isInput
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
']"
).
remove
()
if
!
value
?
field
.
remove
()
# Toggle active class for the tick mark
el
.
addClass
ACTIVE_CLASS
# Toggle the dropdown label
if
@
options
.
toggleLabel
@
updateLabel
(
selectedObject
,
el
,
@
)
if
value
?
if
!
field
.
length
and
fieldName
@
addInput
(
fieldName
,
value
)
else
field
.
val
value
.
trigger
'change'
return
selectedObject
addInput
:
(
fieldName
,
value
)
->
# Create hidden input for form
$input
=
$
(
'<input>'
).
attr
(
'type'
,
'hidden'
)
.
attr
(
'name'
,
fieldName
)
.
val
(
value
)
if
@
options
.
inputId
?
$input
.
attr
(
'id'
,
@
options
.
inputId
)
@
dropdown
.
before
$input
selectRowAtIndex
:
(
e
,
index
)
->
# Dropdown list item link selector, excluding non-selectable list items
selector
=
"
#{
SELECTABLE_CLASSES
}
:eq(
#{
index
}
) a"
if
@
dropdown
.
find
(
".dropdown-toggle-page"
).
length
selector
=
".dropdown-page-one
#{
selector
}
"
# simulate a click on the first link
$el
=
$
(
selector
,
@
dropdown
)
if
$el
.
length
e
.
preventDefault
()
e
.
stopImmediatePropagation
()
$el
.
first
().
trigger
(
'click'
)
href
=
$el
.
attr
'href'
Turbolinks
.
visit
(
href
)
if
href
and
href
isnt
'#'
addArrowKeyEvent
:
->
ARROW_KEY_CODES
=
[
38
,
40
]
$input
=
@
dropdown
.
find
(
".dropdown-input-field"
)
# Dropdown list item selector, excluding non-selectable list items
selector
=
SELECTABLE_CLASSES
if
@
dropdown
.
find
(
'.dropdown-toggle-page'
).
length
selector
=
".dropdown-page-one
#{
selector
}
"
$
(
'body'
).
on
'keydown'
,
(
e
)
=>
currentKeyCode
=
e
.
which
if
ARROW_KEY_CODES
.
indexOf
(
currentKeyCode
)
>=
0
e
.
preventDefault
()
e
.
stopImmediatePropagation
()
PREV_INDEX
=
currentIndex
$listItems
=
$
(
selector
,
@
dropdown
)
# if @options.filterable
# $input.blur()
if
currentKeyCode
is
40
# Move down
currentIndex
+=
1
if
currentIndex
<
(
$listItems
.
length
-
1
)
else
if
currentKeyCode
is
38
# Move up
currentIndex
-=
1
if
currentIndex
>
0
@
highlightRowAtIndex
(
$listItems
,
currentIndex
)
if
currentIndex
isnt
PREV_INDEX
return
false
# If enter is pressed and a row is highlighted, select it
if
currentKeyCode
is
13
and
currentIndex
isnt
-
1
e
.
preventDefault
()
e
.
stopImmediatePropagation
()
@
selectRowAtIndex
e
,
currentIndex
removeArrayKeyEvent
:
->
$
(
'body'
).
off
'keydown'
# Resets the currently selected item row index and removes all highlights
resetRows
:
->
currentIndex
=
-
1
$
(
'.is-focused'
,
@
dropdown
).
removeClass
'is-focused'
highlightRowAtIndex
:
(
$listItems
,
index
)
->
# Remove the class for the previously focused row
$
(
'.is-focused'
,
@
dropdown
).
removeClass
'is-focused'
# Update the class for the row at the specific index
$listItem
=
$listItems
.
eq
(
index
)
$listItem
.
find
(
'a:first-child'
).
addClass
"is-focused"
# Dropdown content scroll area
$dropdownContent
=
$listItem
.
closest
(
'.dropdown-content'
)
dropdownScrollTop
=
$dropdownContent
.
scrollTop
()
dropdownContentHeight
=
$dropdownContent
.
outerHeight
()
dropdownContentTop
=
$dropdownContent
.
prop
(
'offsetTop'
)
dropdownContentBottom
=
dropdownContentTop
+
dropdownContentHeight
# Get the offset bottom of the list item
listItemHeight
=
$listItem
.
outerHeight
()
listItemTop
=
$listItem
.
prop
(
'offsetTop'
)
listItemBottom
=
listItemTop
+
listItemHeight
if
index
is
0
# If this is the first item in the list, scroll to the top
$dropdownContent
.
scrollTop
(
0
)
else
if
index
is
$listItems
.
length
-
1
# If this is the last item in the list, scroll to the bottom
$dropdownContent
.
scrollTop
$dropdownContent
.
prop
'scrollHeight'
else
if
listItemBottom
>
dropdownContentBottom
+
dropdownScrollTop
# Scroll the dropdown content down with a little padding
$dropdownContent
.
scrollTop
(
listItemBottom
-
dropdownContentBottom
+
CURSOR_SELECT_SCROLL_PADDING
)
else
if
listItemTop
<
dropdownContentTop
+
dropdownScrollTop
# Scroll the dropdown content up with a little padding
$dropdownContent
.
scrollTop
(
listItemTop
-
dropdownContentTop
-
CURSOR_SELECT_SCROLL_PADDING
)
updateLabel
:
(
selected
=
null
,
el
=
null
,
instance
=
null
)
=>
$
(
@
el
).
find
(
".dropdown-toggle-text"
).
text
@
options
.
toggleLabel
(
selected
,
el
,
instance
)
$
.
fn
.
glDropdown
=
(
opts
)
->
return
@
.
each
->
if
(
!
$
.
data
@
,
'glDropdown'
)
$
.
data
(
@
,
'glDropdown'
,
new
GitLabDropdown
@
,
opts
)
app/assets/javascripts/search_autocomplete.js
View file @
dcf09a53
...
...
@@ -7,7 +7,9 @@
KEYCODE
=
{
ESCAPE
:
27
,
BACKSPACE
:
8
,
ENTER
:
13
ENTER
:
13
,
UP
:
38
,
DOWN
:
40
};
function
SearchAutocomplete
(
opts
)
{
...
...
@@ -223,6 +225,12 @@
case
KEYCODE
.
ESCAPE
:
this
.
restoreOriginalState
();
break
;
case
KEYCODE
.
ENTER
:
this
.
disableAutocomplete
();
break
;
case
KEYCODE
.
UP
,
case
KEYCODE
.
DOWN
:
return
;
default
:
if
(
this
.
searchInput
.
val
()
===
''
)
{
this
.
disableAutocomplete
();
...
...
@@ -319,9 +327,11 @@
};
SearchAutocomplete
.
prototype
.
disableAutocomplete
=
function
()
{
this
.
searchInput
.
addClass
(
'
disabled
'
);
this
.
dropdown
.
removeClass
(
'
open
'
);
return
this
.
restoreMenu
();
if
(
!
this
.
searchInput
.
hasClass
(
'
disabled
'
)
&&
this
.
dropdown
.
hasClass
(
'
open
'
))
{
this
.
searchInput
.
addClass
(
'
disabled
'
);
this
.
dropdown
.
removeClass
(
'
open
'
).
trigger
(
'
hidden.bs.dropdown
'
);
this
.
restoreMenu
();
}
};
SearchAutocomplete
.
prototype
.
restoreMenu
=
function
()
{
...
...
app/assets/javascripts/search_autocomplete.js.coffee
deleted
100644 → 0
View file @
e74d12a9
class
@
SearchAutocomplete
KEYCODE
=
ESCAPE
:
27
BACKSPACE
:
8
ENTER
:
13
UP
:
38
DOWN
:
40
constructor
:
(
opts
=
{})
->
{
@
wrap
=
$
(
'.search'
)
@
optsEl
=
@
wrap
.
find
(
'.search-autocomplete-opts'
)
@
autocompletePath
=
@
optsEl
.
data
(
'autocomplete-path'
)
@
projectId
=
@
optsEl
.
data
(
'autocomplete-project-id'
)
||
''
@
projectRef
=
@
optsEl
.
data
(
'autocomplete-project-ref'
)
||
''
}
=
opts
# Dropdown Element
@
dropdown
=
@
wrap
.
find
(
'.dropdown'
)
@
dropdownContent
=
@
dropdown
.
find
(
'.dropdown-content'
)
@
locationBadgeEl
=
@
getElement
(
'.location-badge'
)
@
scopeInputEl
=
@
getElement
(
'#scope'
)
@
searchInput
=
@
getElement
(
'.search-input'
)
@
projectInputEl
=
@
getElement
(
'#search_project_id'
)
@
groupInputEl
=
@
getElement
(
'#group_id'
)
@
searchCodeInputEl
=
@
getElement
(
'#search_code'
)
@
repositoryInputEl
=
@
getElement
(
'#repository_ref'
)
@
clearInput
=
@
getElement
(
'.js-clear-input'
)
@
saveOriginalState
()
# Only when user is logged in
@
createAutocomplete
()
if
gon
.
current_user_id
@
searchInput
.
addClass
(
'disabled'
)
@
saveTextLength
()
@
bindEvents
()
# Finds an element inside wrapper element
getElement
:
(
selector
)
->
@
wrap
.
find
(
selector
)
saveOriginalState
:
->
@
originalState
=
@
serializeState
()
saveTextLength
:
->
@
lastTextLength
=
@
searchInput
.
val
().
length
createAutocomplete
:
->
@
searchInput
.
glDropdown
filterInputBlur
:
false
filterable
:
true
filterRemote
:
true
highlight
:
true
enterCallback
:
false
filterInput
:
'input#search'
search
:
fields
:
[
'text'
]
data
:
@
getData
.
bind
(
@
)
selectable
:
true
clicked
:
@
onClick
.
bind
(
@
)
getData
:
(
term
,
callback
)
->
_this
=
@
unless
term
if
contents
=
@
getCategoryContents
()
@
searchInput
.
data
(
'glDropdown'
).
filter
.
options
.
callback
contents
@
enableAutocomplete
()
return
# Prevent multiple ajax calls
return
if
@
loadingSuggestions
@
loadingSuggestions
=
true
jqXHR
=
$
.
get
(
@
autocompletePath
,
{
project_id
:
@
projectId
project_ref
:
@
projectRef
term
:
term
},
(
response
)
->
# Hide dropdown menu if no suggestions returns
if
!
response
.
length
_this
.
disableAutocomplete
()
return
data
=
[]
# List results
firstCategory
=
true
for
suggestion
in
response
# Add group header before list each group
if
lastCategory
isnt
suggestion
.
category
data
.
push
'separator'
if
!
firstCategory
firstCategory
=
false
if
firstCategory
data
.
push
header
:
suggestion
.
category
lastCategory
=
suggestion
.
category
data
.
push
id
:
"
#{
suggestion
.
category
.
toLowerCase
()
}
-
#{
suggestion
.
id
}
"
category
:
suggestion
.
category
text
:
suggestion
.
label
url
:
suggestion
.
url
# Add option to proceed with the search
if
data
.
length
data
.
push
(
'separator'
)
data
.
push
text
:
"Result name contains
\"
#{
term
}
\"
"
url
:
"/search?
\
search=
#{
term
}
\
&project_id=
#{
_this
.
projectInputEl
.
val
()
}
\
&group_id=
#{
_this
.
groupInputEl
.
val
()
}
"
callback
(
data
)
).
always
->
_this
.
loadingSuggestions
=
false
getCategoryContents
:
->
userId
=
gon
.
current_user_id
{
utils
,
projectOptions
,
groupOptions
,
dashboardOptions
}
=
gl
if
utils
.
isInGroupsPage
()
and
groupOptions
options
=
groupOptions
[
utils
.
getGroupSlug
()]
else
if
utils
.
isInProjectPage
()
and
projectOptions
options
=
projectOptions
[
utils
.
getProjectSlug
()]
else
if
dashboardOptions
options
=
dashboardOptions
{
issuesPath
,
mrPath
,
name
}
=
options
items
=
[
{
header
:
"
#{
name
}
"
}
{
text
:
'Issues assigned to me'
,
url
:
"
#{
issuesPath
}
/?assignee_id=
#{
userId
}
"
}
{
text
:
"Issues I've created"
,
url
:
"
#{
issuesPath
}
/?author_id=
#{
userId
}
"
}
'separator'
{
text
:
'Merge requests assigned to me'
,
url
:
"
#{
mrPath
}
/?assignee_id=
#{
userId
}
"
}
{
text
:
"Merge requests I've created"
,
url
:
"
#{
mrPath
}
/?author_id=
#{
userId
}
"
}
]
items
.
splice
0
,
1
unless
name
return
items
serializeState
:
->
{
# Search Criteria
search_project_id
:
@
projectInputEl
.
val
()
group_id
:
@
groupInputEl
.
val
()
search_code
:
@
searchCodeInputEl
.
val
()
repository_ref
:
@
repositoryInputEl
.
val
()
scope
:
@
scopeInputEl
.
val
()
# Location badge
_location
:
@
locationBadgeEl
.
text
()
}
bindEvents
:
->
@
searchInput
.
on
'keydown'
,
@
onSearchInputKeyDown
@
searchInput
.
on
'keyup'
,
@
onSearchInputKeyUp
@
searchInput
.
on
'click'
,
@
onSearchInputClick
@
searchInput
.
on
'focus'
,
@
onSearchInputFocus
@
searchInput
.
on
'blur'
,
@
onSearchInputBlur
@
clearInput
.
on
'click'
,
@
onClearInputClick
@
locationBadgeEl
.
on
'click'
,
=>
@
searchInput
.
focus
()
enableAutocomplete
:
->
# No need to enable anything if user is not logged in
return
if
!
gon
.
current_user_id
unless
@
dropdown
.
hasClass
(
'open'
)
_this
=
@
@
loadingSuggestions
=
false
# If not enabled already, enable
if
not
@
dropdown
.
hasClass
(
'open'
)
# Open dropdown and invoke its opened() method
@
dropdown
.
addClass
(
'open'
)
.
trigger
(
'shown.bs.dropdown'
)
@
searchInput
.
removeClass
(
'disabled'
)
onSearchInputKeyDown
:
=>
# Saves last length of the entered text
@
saveTextLength
()
onSearchInputKeyUp
:
(
e
)
=>
switch
e
.
keyCode
when
KEYCODE
.
BACKSPACE
# when trying to remove the location badge
if
@
lastTextLength
is
0
and
@
badgePresent
()
@
removeLocationBadge
()
# When removing the last character and no badge is present
if
@
lastTextLength
is
1
@
disableAutocomplete
()
# When removing any character from existin value
if
@
lastTextLength
>
1
@
enableAutocomplete
()
when
KEYCODE
.
ESCAPE
@
restoreOriginalState
()
# Close autocomplete on enter
when
KEYCODE
.
ENTER
@
disableAutocomplete
()
when
KEYCODE
.
UP
,
KEYCODE
.
DOWN
return
else
# Handle the case when deleting the input value other than backspace
# e.g. Pressing ctrl + backspace or ctrl + x
if
@
searchInput
.
val
()
is
''
@
disableAutocomplete
()
else
# We should display the menu only when input is not empty
@
enableAutocomplete
()
@
wrap
.
toggleClass
'has-value'
,
!!
e
.
target
.
value
# Avoid falsy value to be returned
return
onSearchInputClick
:
(
e
)
=>
# Prevents closing the dropdown menu
e
.
stopImmediatePropagation
()
onSearchInputFocus
:
=>
@
isFocused
=
true
@
wrap
.
addClass
(
'search-active'
)
@
getData
()
if
@
getValue
()
is
''
getValue
:
->
return
@
searchInput
.
val
()
onClearInputClick
:
(
e
)
=>
e
.
preventDefault
()
@
searchInput
.
val
(
''
).
focus
()
onSearchInputBlur
:
(
e
)
=>
@
isFocused
=
false
@
wrap
.
removeClass
(
'search-active'
)
# If input is blank then restore state
if
@
searchInput
.
val
()
is
''
@
restoreOriginalState
()
addLocationBadge
:
(
item
)
->
category
=
if
item
.
category
?
then
"
#{
item
.
category
}
: "
else
''
value
=
if
item
.
value
?
then
item
.
value
else
''
badgeText
=
"
#{
category
}#{
value
}
"
@
locationBadgeEl
.
text
(
badgeText
).
show
()
@
wrap
.
addClass
(
'has-location-badge'
)
hasLocationBadge
:
->
return
@
wrap
.
is
'.has-location-badge'
restoreOriginalState
:
->
inputs
=
Object
.
keys
@
originalState
for
input
in
inputs
@
getElement
(
"#
#{
input
}
"
).
val
(
@
originalState
[
input
])
if
@
originalState
.
_location
is
''
@
locationBadgeEl
.
hide
()
else
@
addLocationBadge
(
value
:
@
originalState
.
_location
)
badgePresent
:
->
@
locationBadgeEl
.
length
resetSearchState
:
->
inputs
=
Object
.
keys
@
originalState
for
input
in
inputs
# _location isnt a input
break
if
input
is
'_location'
@
getElement
(
"#
#{
input
}
"
).
val
(
''
)
removeLocationBadge
:
->
@
locationBadgeEl
.
hide
()
@
resetSearchState
()
@
wrap
.
removeClass
(
'has-location-badge'
)
@
disableAutocomplete
()
disableAutocomplete
:
->
# If not disabled already, disable
if
not
@
searchInput
.
hasClass
(
'disabled'
)
and
@
dropdown
.
hasClass
'open'
@
searchInput
.
addClass
(
'disabled'
)
# Close dropdown and invoke its hidden() method
@
dropdown
.
removeClass
(
'open'
).
trigger
'hidden.bs.dropdown'
@
restoreMenu
()
restoreMenu
:
->
html
=
"<ul>
<li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
</ul>"
@
dropdownContent
.
html
(
html
)
onClick
:
(
item
,
$el
,
e
)
->
if
location
.
pathname
.
indexOf
(
item
.
url
)
isnt
-
1
e
.
preventDefault
()
if
not
@
badgePresent
if
item
.
category
is
'Projects'
@
projectInputEl
.
val
(
item
.
id
)
@
addLocationBadge
(
value
:
'This project'
)
if
item
.
category
is
'Groups'
@
groupInputEl
.
val
(
item
.
id
)
@
addLocationBadge
(
value
:
'This group'
)
$el
.
removeClass
(
'is-active'
)
@
disableAutocomplete
()
@
searchInput
.
val
(
''
).
focus
()
spec/javascripts/gl_dropdown_spec.js.coffee
deleted
100644 → 0
View file @
e74d12a9
#= require jquery
#= require gl_dropdown
#= require turbolinks
#= require lib/utils/common_utils
#= require lib/utils/type_utility
NON_SELECTABLE_CLASSES
=
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'
ITEM_SELECTOR
=
".dropdown-content li:not(
#{
NON_SELECTABLE_CLASSES
}
)"
FOCUSED_ITEM_SELECTOR
=
ITEM_SELECTOR
+
' a.is-focused'
ARROW_KEYS
=
DOWN
:
40
UP
:
38
ENTER
:
13
ESC
:
27
navigateWithKeys
=
(
direction
,
steps
,
cb
,
i
)
->
i
=
i
||
0
$
(
'body'
).
trigger
type
:
'keydown'
which
:
ARROW_KEYS
[
direction
.
toUpperCase
()]
keyCode
:
ARROW_KEYS
[
direction
.
toUpperCase
()]
i
++
if
i
<=
steps
navigateWithKeys
direction
,
steps
,
cb
,
i
else
cb
()
initDropdown
=
->
@
dropdownContainerElement
=
$
(
'.dropdown.inline'
)
@
dropdownMenuElement
=
$
(
'.dropdown-menu'
,
@
dropdownContainerElement
)
@
projectsData
=
fixture
.
load
(
'projects.json'
)[
0
]
@
dropdownButtonElement
=
$
(
'#js-project-dropdown'
,
@
dropdownContainerElement
).
glDropdown
selectable
:
true
data
:
@
projectsData
text
:
(
project
)
->
(
project
.
name_with_namespace
or
project
.
name
)
id
:
(
project
)
->
project
.
id
describe
'Dropdown'
,
->
fixture
.
preload
'gl_dropdown.html'
fixture
.
preload
'projects.json'
beforeEach
->
fixture
.
load
'gl_dropdown.html'
initDropdown
.
call
this
afterEach
->
$
(
'body'
).
unbind
'keydown'
@
dropdownContainerElement
.
unbind
'keyup'
it
'should open on click'
,
->
expect
(
@
dropdownContainerElement
).
not
.
toHaveClass
'open'
@
dropdownButtonElement
.
click
()
expect
(
@
dropdownContainerElement
).
toHaveClass
'open'
describe
'that is open'
,
->
beforeEach
->
@
dropdownButtonElement
.
click
()
it
'should select a following item on DOWN keypress'
,
->
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
@
dropdownMenuElement
).
length
).
toBe
0
randomIndex
=
Math
.
floor
(
Math
.
random
()
*
(
@
projectsData
.
length
-
1
))
+
0
navigateWithKeys
'down'
,
randomIndex
,
=>
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
@
dropdownMenuElement
).
length
).
toBe
1
expect
(
$
(
"
#{
ITEM_SELECTOR
}
:eq(
#{
randomIndex
}
) a"
,
@
dropdownMenuElement
)).
toHaveClass
'is-focused'
it
'should select a previous item on UP keypress'
,
->
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
@
dropdownMenuElement
).
length
).
toBe
0
navigateWithKeys
'down'
,
(
@
projectsData
.
length
-
1
),
=>
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
@
dropdownMenuElement
).
length
).
toBe
1
randomIndex
=
Math
.
floor
(
Math
.
random
()
*
(
@
projectsData
.
length
-
2
))
+
0
navigateWithKeys
'up'
,
randomIndex
,
=>
expect
(
$
(
FOCUSED_ITEM_SELECTOR
,
@
dropdownMenuElement
).
length
).
toBe
1
expect
(
$
(
"
#{
ITEM_SELECTOR
}
:eq(
#{
((
@
projectsData
.
length
-
2
)
-
randomIndex
)
}
) a"
,
@
dropdownMenuElement
)).
toHaveClass
'is-focused'
it
'should click the selected item on ENTER keypress'
,
->
expect
(
@
dropdownContainerElement
).
toHaveClass
'open'
randomIndex
=
Math
.
floor
(
Math
.
random
()
*
(
@
projectsData
.
length
-
1
))
+
0
navigateWithKeys
'down'
,
randomIndex
,
=>
spyOn
(
Turbolinks
,
'visit'
).
and
.
stub
()
navigateWithKeys
'enter'
,
null
,
=>
expect
(
@
dropdownContainerElement
).
not
.
toHaveClass
'open'
link
=
$
(
"
#{
ITEM_SELECTOR
}
:eq(
#{
randomIndex
}
) a"
,
@
dropdownMenuElement
)
expect
(
link
).
toHaveClass
'is-active'
linkedLocation
=
link
.
attr
'href'
if
linkedLocation
and
linkedLocation
isnt
'#'
expect
(
Turbolinks
.
visit
).
toHaveBeenCalledWith
linkedLocation
it
'should close on ESC keypress'
,
->
expect
(
@
dropdownContainerElement
).
toHaveClass
'open'
@
dropdownContainerElement
.
trigger
type
:
'keyup'
which
:
ARROW_KEYS
.
ESC
keyCode
:
ARROW_KEYS
.
ESC
expect
(
@
dropdownContainerElement
).
not
.
toHaveClass
'open'
spec/javascripts/gl_dropdown_spec.js.es6
0 → 100644
View file @
dcf09a53
/*= require jquery */
/*= require gl_dropdown */
/*= require turbolinks */
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
const ARROW_KEYS = {
DOWN: 40,
UP: 38,
ENTER: 13,
ESC: 27
};
var navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
$('body').trigger({
type: 'keydown',
which: ARROW_KEYS[direction.toUpperCase()],
keyCode: ARROW_KEYS[direction.toUpperCase()]
});
i++;
if (i <= steps) {
navigateWithKeys(direction, steps, cb, i);
} else {
cb();
}
};
var initDropdown = function initDropdown() {
this.dropdownContainerElement = $('.dropdown.inline');
this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
this.projectsData = fixture.load('projects.json')[0];
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
selectable: true,
data: this.projectsData,
text: (project) => {
(project.name_with_namespace || project.name)
},
id: (project) => {
project.id
}
});
};
describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html');
fixture.preload('projects.json');
function beforeEach() {
fixture.load('gl_dropdown.html');
initDropdown.call(this);
}
function afterEach() {
$('body').unbind('keydown');
this.dropdownContainerElement.unbind('keyup');
}
it('should open on click', () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
this.dropdownButtonElement.click();
expect(this.dropdownContainerElement).toHaveClass('open');
});
describe('that is open', function describeThatIsOpen() {
function beforeEach() {
this.dropdownButtonElement.click();
}
it('should select a following item on DOWN keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
});
});
it('should select a previous item on UP keypress', () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
});
});
});
it('should click the selected item on ENTER keypress', () => {
expect(this.dropdownContainerElement).toHaveClass('open')
let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
navigateWithKeys('down', randomIndex, () => {
spyOn(Turbolinks, 'visit').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
expect(link).toHaveClass('is-active');
let linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
});
});
});
it('should close on ESC keypress', () => {
expect(this.dropdownContainerElement).toHaveClass('open');
this.dropdownContainerElement.trigger({
type: 'keyup',
which: ARROW_KEYS.ESC,
keyCode: ARROW_KEYS.ESC
});
expect(this.dropdownContainerElement).not.toHaveClass('open');
});
});
});
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