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
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
Expand all
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
This diff is collapsed.
Click to expand it.
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