Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
88af141c
Commit
88af141c
authored
Mar 17, 2015
by
Randall Leeds
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2042 from hypothesis/lookahead-tags-3
Autocomplete feature for tags
parents
d6f88ff4
a87877b0
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
766 additions
and
301 deletions
+766
-301
app.coffee
h/static/scripts/app.coffee
+1
-0
annotation.coffee
h/static/scripts/directives/annotation.coffee
+19
-4
privacy.coffee
h/static/scripts/directives/privacy.coffee
+3
-21
annotation-test.coffee
h/static/scripts/directives/test/annotation-test.coffee
+7
-0
privacy-test.coffee
h/static/scripts/directives/test/privacy-test.coffee
+17
-36
helpers.coffee
h/static/scripts/helpers/helpers.coffee
+1
-0
tag-helpers.coffee
h/static/scripts/helpers/tag-helpers.coffee
+54
-0
tag-helpers-test.coffee
h/static/scripts/helpers/test/tag-helpers-test.coffee
+112
-0
local-storage-service.coffee
h/static/scripts/local-storage-service.coffee
+38
-0
local-storage-service-test.coffee
h/static/scripts/test/local-storage-service-test.coffee
+85
-0
ng-tags-input.js
h/static/scripts/vendor/ng-tags-input.js
+377
-239
tags-input.scss
h/static/styles/tags-input.scss
+47
-0
annotation.html
h/templates/client/annotation.html
+5
-1
No files found.
h/static/scripts/app.coffee
View file @
88af141c
...
...
@@ -126,6 +126,7 @@ require('./auth-service')
require
(
'./cross-frame-service'
)
require
(
'./flash-service'
)
require
(
'./permissions-service'
)
require
(
'./local-storage-service'
)
require
(
'./store-service'
)
require
(
'./threading-service'
)
...
...
h/static/scripts/directives/annotation.coffee
View file @
88af141c
...
...
@@ -29,12 +29,13 @@ validate = (value) ->
# {@link annotationMapper AnnotationMapper service} for persistence.
###
AnnotationController = [
'$scope', '$timeout', '$rootScope', '$document',
'auth', 'drafts', 'flash', 'permissions',
'$scope', '$timeout', '$
q', '$
rootScope', '$document',
'auth', 'drafts', 'flash', 'permissions',
'tagHelpers',
'timeHelpers', 'annotationUI', 'annotationMapper'
($scope, $timeout, $rootScope, $document,
auth, drafts, flash, permissions,
($scope, $timeout, $
q, $
rootScope, $document,
auth, drafts, flash, permissions,
tagHelpers,
timeHelpers, annotationUI, annotationMapper) ->
@annotation = {}
@action = 'view'
@document = null
...
...
@@ -50,6 +51,15 @@ AnnotationController = [
original = null
vm = this
###
*
# @ngdoc method
# @name annotation.AnnotationController#tagsAutoComplete.
# @returns {Promise} immediately resolved to {string[]} -
# the tags to show in autocomplete.
###
this.tagsAutoComplete = (query) ->
$q.when(tagHelpers.filterTags(query))
###
*
# @ngdoc method
# @name annotation.AnnotationController#isComment.
...
...
@@ -145,6 +155,11 @@ AnnotationController = [
unless validate(@annotation)
return flash 'info', 'Please add text or a tag before publishing.'
# Update stored tags with the new tags of this annotation
tags = @annotation.tags.filter (tag) ->
tag.text not in (model.tags or [])
tagHelpers.storeTags(tags)
angular.extend model, @annotation,
tags: (tag.text for tag in @annotation.tags)
...
...
h/static/scripts/directives/privacy.coffee
View file @
88af141c
privacy
=
[
'
$window'
,
'permissions'
,
(
$window
,
permissions
)
->
privacy
=
[
'
localstorage'
,
'permissions'
,
(
localstorage
,
permissions
)
->
VISIBILITY_KEY
=
'hypothesis.visibility'
VISIBILITY_PUBLIC
=
'public'
VISIBILITY_PRIVATE
=
'private'
...
...
@@ -16,24 +16,6 @@ privacy = ['$window', 'permissions', ($window, permissions) ->
isPublic
=
(
level
)
->
level
==
VISIBILITY_PUBLIC
# Detection is needed because we run often as a third party widget and
# third party storage blocking often blocks cookies and local storage
# https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js
storage
=
do
->
key
=
'hypothesis.testKey'
try
$window
.
localStorage
.
setItem
key
,
key
$window
.
localStorage
.
removeItem
key
$window
.
localStorage
catch
memoryStorage
=
{}
getItem
:
(
key
)
->
if
key
of
memoryStorage
then
memoryStorage
[
key
]
else
null
setItem
:
(
key
,
value
)
->
memoryStorage
[
key
]
=
value
removeItem
:
(
key
)
->
delete
memoryStorage
[
key
]
link
:
(
scope
,
elem
,
attrs
,
controller
)
->
return
unless
controller
?
...
...
@@ -62,7 +44,7 @@ privacy = ['$window', 'permissions', ($window, permissions) ->
controller
.
$render
=
->
unless
controller
.
$modelValue
.
read
?
.
length
name
=
storage
.
getItem
VISIBILITY_KEY
name
=
local
storage
.
getItem
VISIBILITY_KEY
name
?=
VISIBILITY_PUBLIC
level
=
getLevel
(
name
)
controller
.
$setViewValue
level
...
...
@@ -71,7 +53,7 @@ privacy = ['$window', 'permissions', ($window, permissions) ->
scope
.
levels
=
levels
scope
.
setLevel
=
(
level
)
->
storage
.
setItem
VISIBILITY_KEY
,
level
.
name
local
storage
.
setItem
VISIBILITY_KEY
,
level
.
name
controller
.
$setViewValue
level
controller
.
$render
()
scope
.
isPublic
=
isPublic
...
...
h/static/scripts/directives/test/annotation-test.coffee
View file @
88af141c
...
...
@@ -19,6 +19,7 @@ describe 'h.directives.annotation', ->
fakePermissions
=
null
fakePersonaFilter
=
null
fakeStore
=
null
fakeTagHelpers
=
null
fakeTimeHelpers
=
null
fakeUrlEncodeFilter
=
null
sandbox
=
null
...
...
@@ -48,6 +49,7 @@ describe 'h.directives.annotation', ->
remove
:
sandbox
.
stub
()
}
fakeFlash
=
sandbox
.
stub
()
fakeMomentFilter
=
sandbox
.
stub
().
returns
(
'ages ago'
)
fakePermissions
=
{
isPublic
:
sandbox
.
stub
().
returns
(
true
)
...
...
@@ -57,6 +59,10 @@ describe 'h.directives.annotation', ->
private
:
sandbox
.
stub
().
returns
({
read
:
[
'justme'
]})
}
fakePersonaFilter
=
sandbox
.
stub
().
returnsArg
(
0
)
fakeTagsHelpers
=
{
filterTags
:
sandbox
.
stub
().
returns
(
'a while ago'
)
refreshTags
:
sandbox
.
stub
().
returns
(
30
)
}
fakeTimeHelpers
=
{
toFuzzyString
:
sandbox
.
stub
().
returns
(
'a while ago'
)
nextFuzzyUpdate
:
sandbox
.
stub
().
returns
(
30
)
...
...
@@ -72,6 +78,7 @@ describe 'h.directives.annotation', ->
$provide
.
value
'permissions'
,
fakePermissions
$provide
.
value
'personaFilter'
,
fakePersonaFilter
$provide
.
value
'store'
,
fakeStore
$provide
.
value
'tagHelpers'
,
fakeTagHelpers
$provide
.
value
'timeHelpers'
,
fakeTimeHelpers
$provide
.
value
'urlencodeFilter'
,
fakeUrlEncodeFilter
return
...
...
h/static/scripts/directives/test/privacy-test.coffee
View file @
88af141c
...
...
@@ -12,6 +12,7 @@ describe 'h.directives.privacy', ->
$window
=
null
fakeAuth
=
null
fakePermissions
=
null
fakeLocalStorage
=
null
sandbox
=
null
before
->
...
...
@@ -28,6 +29,13 @@ describe 'h.directives.privacy', ->
user
:
'acct:angry.joe@texas.com'
}
storage
=
{}
fakeLocalStorage
=
{
getItem
:
sandbox
.
spy
(
key
)
->
storage
[
key
]
setItem
:
sandbox
.
spy
(
key
,
value
)
->
storage
[
key
]
=
value
removeItem
:
sandbox
.
spy
(
key
)
->
delete
storage
[
key
]
}
fakePermissions
=
{
isPublic
:
sandbox
.
stub
().
returns
(
true
)
isPrivate
:
sandbox
.
stub
().
returns
(
false
)
...
...
@@ -37,6 +45,7 @@ describe 'h.directives.privacy', ->
}
$provide
.
value
'auth'
,
fakeAuth
$provide
.
value
'localstorage'
,
fakeLocalStorage
$provide
.
value
'permissions'
,
fakePermissions
return
...
...
@@ -48,31 +57,7 @@ describe 'h.directives.privacy', ->
afterEach
->
sandbox
.
restore
()
describe
'memory fallback'
,
->
$scope2
=
null
beforeEach
inject
(
_$rootScope_
)
->
$scope2
=
_$rootScope_
.
$new
()
$window
.
localStorage
=
null
it
'stores the default visibility level when it changes'
,
->
$scope
.
permissions
=
{
read
:
[
'acct:user@example.com'
]}
$element
=
$compile
(
'<privacy ng-model="permissions">'
)(
$scope
)
$scope
.
$digest
()
$isolateScope
=
$element
.
isolateScope
()
$isolateScope
.
setLevel
(
name
:
VISIBILITY_PUBLIC
)
$scope2
.
permissions
=
{
read
:
[]}
$element
=
$compile
(
'<privacy ng-model="permissions">'
)(
$scope2
)
$scope2
.
$digest
()
# Roundabout way: the storage works because the directive
# could read out the privacy level
readPermissions
=
$scope2
.
permissions
.
read
[
0
]
assert
.
equal
readPermissions
,
'everybody'
describe
'has localStorage'
,
->
describe
'saves visibility level'
,
->
it
'stores the default visibility level when it changes'
,
->
$scope
.
permissions
=
{
read
:
[
'acct:user@example.com'
]}
...
...
@@ -82,19 +67,15 @@ describe 'h.directives.privacy', ->
$isolateScope
.
setLevel
(
name
:
VISIBILITY_PUBLIC
)
expected
=
VISIBILITY_PUBLIC
stored
=
$window
.
l
ocalStorage
.
getItem
VISIBILITY_KEY
stored
=
fakeL
ocalStorage
.
getItem
VISIBILITY_KEY
assert
.
equal
stored
,
expected
describe
'setting permissions'
,
->
$element
=
null
store
=
null
beforeEach
->
store
=
$window
.
localStorage
describe
'when no setting is stored'
,
->
beforeEach
->
stor
e
.
removeItem
VISIBILITY_KEY
fakeLocalStorag
e
.
removeItem
VISIBILITY_KEY
it
'defaults to public'
,
->
$scope
.
permissions
=
{
read
:
[]}
...
...
@@ -105,7 +86,7 @@ describe 'h.directives.privacy', ->
describe
'when permissions.read is empty'
,
->
beforeEach
->
stor
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PUBLIC
fakeLocalStorag
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PUBLIC
$scope
.
permissions
=
{
read
:
[]}
$element
=
$compile
(
'<privacy ng-model="permissions">'
)(
$scope
)
...
...
@@ -115,14 +96,14 @@ describe 'h.directives.privacy', ->
assert
.
equal
$element
.
isolateScope
().
level
.
name
,
VISIBILITY_PUBLIC
it
'does not alter the level on subsequent renderings'
,
->
stor
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PRIVATE
fakeLocalStorag
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PRIVATE
$scope
.
permissions
.
read
=
[
'acct:user@example.com'
]
$scope
.
$digest
()
assert
.
equal
$element
.
isolateScope
().
level
.
name
,
VISIBILITY_PUBLIC
describe
'when permissions.read is filled'
,
->
it
'does not alter the level'
,
->
stor
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PRIVATE
fakeLocalStorag
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PRIVATE
$scope
.
permissions
=
{
read
:
[
'group:__world__'
]}
$element
=
$compile
(
'<privacy ng-model="permissions">'
)(
$scope
)
...
...
@@ -135,14 +116,14 @@ describe 'h.directives.privacy', ->
$scope
.
permissions
=
{
read
:
[]}
it
'fills the permissions fields with the auth.user name'
,
->
stor
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PRIVATE
fakeLocalStorag
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PRIVATE
$element
=
$compile
(
'<privacy ng-model="permissions">'
)(
$scope
)
$scope
.
$digest
()
assert
.
deepEqual
$scope
.
permissions
,
fakePermissions
.
private
()
it
'puts group_world into the read permissions for public visibility'
,
->
stor
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PUBLIC
fakeLocalStorag
e
.
setItem
VISIBILITY_KEY
,
VISIBILITY_PUBLIC
$element
=
$compile
(
'<privacy ng-model="permissions">'
)(
$scope
)
$scope
.
$digest
()
...
...
h/static/scripts/helpers/helpers.coffee
View file @
88af141c
...
...
@@ -4,6 +4,7 @@ angular.module('h.helpers', ['bootstrap'])
require
(
'./form-helpers'
)
require
(
'./string-helpers'
)
require
(
'./tag-helpers'
)
require
(
'./time-helpers'
)
require
(
'./ui-helpers'
)
require
(
'./xsrf-service'
)
h/static/scripts/helpers/tag-helpers.coffee
0 → 100644
View file @
88af141c
createTagHelpers
=
[
'localstorage'
,
(
localstorage
)
->
TAGS_LIST_KEY
=
'hypothesis.user.tags.list'
TAGS_MAP_KEY
=
'hypothesis.user.tags.map'
filterTags
:
(
query
)
->
savedTags
=
localstorage
.
getObject
TAGS_LIST_KEY
savedTags
?=
[]
# Only show tags having query as a substring
filterFn
=
(
e
)
->
e
.
toLowerCase
().
indexOf
(
query
.
toLowerCase
())
>
-
1
savedTags
.
filter
(
filterFn
)
# Add newly added tags from an annotation to the stored ones and refresh
# timestamp for every tags used.
storeTags
:
(
tags
)
->
savedTags
=
localstorage
.
getObject
TAGS_MAP_KEY
savedTags
?=
{}
for
tag
in
tags
if
savedTags
[
tag
.
text
]
?
# Update counter and timestamp
savedTags
[
tag
.
text
].
count
+=
1
savedTags
[
tag
.
text
].
updated
=
Date
.
now
()
else
# Brand new tag, create an entry for it
savedTags
[
tag
.
text
]
=
{
text
:
tag
.
text
count
:
1
updated
:
Date
.
now
()
}
localstorage
.
setObject
TAGS_MAP_KEY
,
savedTags
tagsList
=
[]
for
tag
of
savedTags
tagsList
[
tagsList
.
length
]
=
tag
# Now produce TAGS_LIST, ordered by (count desc, lexical asc)
compareFn
=
(
t1
,
t2
)
->
if
savedTags
[
t1
].
count
!=
savedTags
[
t2
].
count
return
savedTags
[
t2
].
count
-
savedTags
[
t1
].
count
else
return
-
1
if
t1
<
t2
return
1
if
t1
>
t2
return
0
tagsList
=
tagsList
.
sort
(
compareFn
)
localstorage
.
setObject
TAGS_LIST_KEY
,
tagsList
]
angular
.
module
(
'h.helpers'
)
.
service
(
'tagHelpers'
,
createTagHelpers
)
h/static/scripts/helpers/test/tag-helpers-test.coffee
0 → 100644
View file @
88af141c
{
module
,
inject
}
=
require
(
'angular-mock'
)
assert
=
chai
.
assert
describe
'h.helpers:tag-helpers'
,
->
TAGS_LIST_KEY
=
'hypothesis.user.tags.list'
TAGS_MAP_KEY
=
'hypothesis.user.tags.map'
fakeLocalStorage
=
null
sandbox
=
null
savedTagsMap
=
null
savedTagsList
=
null
tagHelpers
=
null
before
->
angular
.
module
(
'h.helpers'
,
[])
require
(
'../tag-helpers'
)
beforeEach
module
(
'h.helpers'
)
beforeEach
module
(
$provide
)
->
sandbox
=
sinon
.
sandbox
.
create
()
fakeStorage
=
{}
fakeLocalStorage
=
{
getObject
:
sandbox
.
spy
(
key
)
->
fakeStorage
[
key
]
setObject
:
sandbox
.
spy
(
key
,
value
)
->
fakeStorage
[
key
]
=
value
wipe
:
->
fakeStorage
=
{}
}
$provide
.
value
'localstorage'
,
fakeLocalStorage
return
beforeEach
inject
(
_tagHelpers_
)
->
tagHelpers
=
_tagHelpers_
afterEach
->
sandbox
.
restore
()
beforeEach
->
fakeLocalStorage
.
wipe
()
stamp
=
Date
.
now
()
savedTagsMap
=
foo
:
text
:
'foo'
count
:
1
updated
:
stamp
bar
:
text
:
'bar'
count
:
5
updated
:
stamp
future
:
text
:
'future'
count
:
2
updated
:
stamp
argon
:
text
:
'argon'
count
:
1
updated
:
stamp
savedTagsList
=
[
'bar'
,
'future'
,
'argon'
,
'foo'
]
fakeLocalStorage
.
setObject
TAGS_MAP_KEY
,
savedTagsMap
fakeLocalStorage
.
setObject
TAGS_LIST_KEY
,
savedTagsList
describe
'filterTags()'
,
->
it
'returns tags having the query as a substring'
,
->
tags
=
tagHelpers
.
filterTags
(
'a'
)
assert
.
deepEqual
(
tags
,
[
'bar'
,
'argon'
])
it
'is case insensitive'
,
->
tags
=
tagHelpers
.
filterTags
(
'Ar'
)
assert
.
deepEqual
(
tags
,
[
'bar'
,
'argon'
])
describe
'storeTags()'
,
->
it
'saves new tags to storage'
,
->
tags
=
[{
text
:
'new'
}]
tagHelpers
.
storeTags
(
tags
)
storedTagsList
=
fakeLocalStorage
.
getObject
TAGS_LIST_KEY
assert
.
deepEqual
(
storedTagsList
,
[
'bar'
,
'future'
,
'argon'
,
'foo'
,
'new'
])
storedTagsMap
=
fakeLocalStorage
.
getObject
TAGS_MAP_KEY
assert
.
isTrue
(
storedTagsMap
.
new
?
)
assert
.
equal
(
storedTagsMap
.
new
.
count
,
1
)
assert
.
equal
(
storedTagsMap
.
new
.
text
,
'new'
)
it
'increases the count for a tag already stored'
,
->
tags
=
[{
text
:
'bar'
}]
tagHelpers
.
storeTags
(
tags
)
storedTagsMap
=
fakeLocalStorage
.
getObject
TAGS_MAP_KEY
assert
.
equal
(
storedTagsMap
.
bar
.
count
,
6
)
it
'list is ordered by count desc, lexical asc'
,
->
# Will increase from 1 to 6 (as future)
tags
=
[{
text
:
'foo'
}]
tagHelpers
.
storeTags
(
tags
)
tagHelpers
.
storeTags
(
tags
)
tagHelpers
.
storeTags
(
tags
)
tagHelpers
.
storeTags
(
tags
)
tagHelpers
.
storeTags
(
tags
)
storedTagsList
=
fakeLocalStorage
.
getObject
TAGS_LIST_KEY
assert
.
deepEqual
(
storedTagsList
,
[
'foo'
,
'bar'
,
'future'
,
'argon'
])
it
'gets/sets its objects from the localstore'
,
->
tags
=
[{
text
:
'foo'
}]
tagHelpers
.
storeTags
(
tags
)
assert
.
called
(
fakeLocalStorage
.
getObject
)
assert
.
called
(
fakeLocalStorage
.
setObject
)
h/static/scripts/local-storage-service.coffee
0 → 100644
View file @
88af141c
localstorage
=
[
'$window'
,
(
$window
)
->
# Detection is needed because we run often as a third party widget and
# third party storage blocking often blocks cookies and local storage
# https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js
storage
=
do
->
key
=
'hypothesis.testKey'
try
$window
.
localStorage
.
setItem
key
,
key
$window
.
localStorage
.
removeItem
key
$window
.
localStorage
catch
memoryStorage
=
{}
getItem
:
(
key
)
->
if
key
of
memoryStorage
then
memoryStorage
[
key
]
else
null
setItem
:
(
key
,
value
)
->
memoryStorage
[
key
]
=
value
removeItem
:
(
key
)
->
delete
memoryStorage
[
key
]
return
{
getItem
:
(
key
)
->
storage
.
getItem
key
getObject
:
(
key
)
->
json
=
storage
.
getItem
key
return
JSON
.
parse
json
if
json
null
setItem
:
(
key
,
value
)
->
storage
.
setItem
key
,
value
setObject
:
(
key
,
value
)
->
repr
=
JSON
.
stringify
value
storage
.
setItem
key
,
repr
removeItem
:
(
key
)
->
storage
.
removeItem
key
}
]
angular
.
module
(
'h'
)
.
service
(
'localstorage'
,
localstorage
)
h/static/scripts/test/local-storage-service-test.coffee
0 → 100644
View file @
88af141c
{
module
,
inject
}
=
require
(
'angular-mock'
)
assert
=
chai
.
assert
sinon
.
assert
.
expose
assert
,
prefix
:
null
describe
'h:localstorage'
,
->
fakeWindow
=
null
sandbox
=
null
before
->
angular
.
module
(
'h'
,
[])
require
(
'../local-storage-service'
)
beforeEach
module
(
'h'
)
describe
'memory fallback'
,
->
localstorage
=
null
key
=
null
beforeEach
module
(
$provide
)
->
sandbox
=
sinon
.
sandbox
.
create
()
fakeWindow
=
{
localStorage
:
{}
}
$provide
.
value
'$window'
,
fakeWindow
return
afterEach
->
sandbox
.
restore
()
beforeEach
inject
(
_localstorage_
)
->
localstorage
=
_localstorage_
key
=
'test.memory.key'
it
'sets/gets Item'
,
->
value
=
'What shall we do with a drunken sailor?'
localstorage
.
setItem
key
,
value
actual
=
localstorage
.
getItem
key
assert
.
equal
value
,
actual
it
'removes item'
,
->
localstorage
.
setItem
key
,
''
localstorage
.
removeItem
key
result
=
localstorage
.
getItem
key
assert
.
isNull
result
it
'sets/gets Object'
,
->
data
=
{
'foo'
:
'bar'
}
localstorage
.
setObject
key
,
data
stringified
=
localstorage
.
getItem
key
assert
.
equal
stringified
,
JSON
.
stringify
data
actual
=
localstorage
.
getObject
key
assert
.
deepEqual
actual
,
data
describe
'browser localStorage'
,
->
localstorage
=
null
beforeEach
module
(
$provide
)
->
sandbox
=
sinon
.
sandbox
.
create
()
fakeWindow
=
{
localStorage
:
{
getItem
:
sandbox
.
stub
()
setItem
:
sandbox
.
stub
()
removeItem
:
sandbox
.
stub
()
}
}
$provide
.
value
'$window'
,
fakeWindow
return
afterEach
->
sandbox
.
restore
()
beforeEach
inject
(
_localstorage_
)
->
localstorage
=
_localstorage_
it
'uses window.localStorage functions to handle data'
,
->
key
=
'test.storage.key'
data
=
'test data'
localstorage
.
setItem
key
,
data
assert
.
calledWith
fakeWindow
.
localStorage
.
setItem
,
key
,
data
h/static/scripts/vendor/ng-tags-input.js
View file @
88af141c
/*!
* ngTagsInput v2.
0.1
* ngTagsInput v2.
2.0
* http://mbenford.github.io/ngTagsInput
*
* Copyright (c) 2013-201
4
Michael Benford
* Copyright (c) 2013-201
5
Michael Benford
* License: MIT
*
* Generated at 201
4-04-13 21:25:38
-0300
* Generated at 201
5-03-02 01:54:50
-0300
*/
(
function
()
{
'use strict'
;
...
...
@@ -21,55 +21,8 @@ var KEYS = {
comma
:
188
};
function
SimplePubSub
()
{
var
events
=
{};
return
{
on
:
function
(
names
,
handler
)
{
names
.
split
(
' '
).
forEach
(
function
(
name
)
{
if
(
!
events
[
name
])
{
events
[
name
]
=
[];
}
events
[
name
].
push
(
handler
);
});
return
this
;
},
trigger
:
function
(
name
,
args
)
{
angular
.
forEach
(
events
[
name
],
function
(
handler
)
{
handler
.
call
(
null
,
args
);
});
return
this
;
}
};
}
function
makeObjectArray
(
array
,
key
)
{
array
=
array
||
[];
if
(
array
.
length
>
0
&&
!
angular
.
isObject
(
array
[
0
]))
{
array
.
forEach
(
function
(
item
,
index
)
{
array
[
index
]
=
{};
array
[
index
][
key
]
=
item
;
});
}
return
array
;
}
function
findInObjectArray
(
array
,
obj
,
key
)
{
var
item
=
null
;
for
(
var
i
=
0
;
i
<
array
.
length
;
i
++
)
{
// I'm aware of the internationalization issues regarding toLowerCase()
// but I couldn't come up with a better solution right now
if
(
array
[
i
][
key
].
toLowerCase
()
===
obj
[
key
].
toLowerCase
())
{
item
=
array
[
i
];
break
;
}
}
return
item
;
}
function
replaceAll
(
str
,
substr
,
newSubstr
)
{
var
expression
=
substr
.
replace
(
/
([
.?*+^$[
\]\\
(){}|-
])
/g
,
'
\\
$1'
);
return
str
.
replace
(
new
RegExp
(
expression
,
'gi'
),
newSubstr
);
}
var
MAX_SAFE_INTEGER
=
9007199254740991
;
var
SUPPORTED_INPUT_TYPES
=
[
'text'
,
'email'
,
'url'
];
var
tagsInput
=
angular
.
module
(
'ngTagsInput'
,
[]);
...
...
@@ -83,12 +36,13 @@ var tagsInput = angular.module('ngTagsInput', []);
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} [displayProperty=text] Property to be rendered as the tag label.
* @param {string=} [type=text] Type of the input element. Only 'text', 'email' and 'url' are supported values.
* @param {number=} tabindex Tab order of the control.
* @param {string=} [placeholder=Add a tag] Placeholder text for the control.
* @param {number=} [minLength=3] Minimum length for a new tag.
* @param {number=}
maxLength
Maximum length allowed for a new tag.
* @param {number=}
minTags
Sets minTags validation error key if the number of tags added is less than minTags.
* @param {number=}
maxTags
Sets maxTags validation error key if the number of tags added is greater than maxTags.
* @param {number=}
[maxLength=MAX_SAFE_INTEGER]
Maximum length allowed for a new tag.
* @param {number=}
[minTags=0]
Sets minTags validation error key if the number of tags added is less than minTags.
* @param {number=}
[maxTags=MAX_SAFE_INTEGER]
Sets maxTags validation error key if the number of tags added is greater than maxTags.
* @param {boolean=} [allowLeftoverText=false] Sets leftoverText validation error key if there is any leftover text in
* the input element when the directive loses focus.
* @param {string=} [removeTagSymbol=×] Symbol character for the remove tag button.
...
...
@@ -96,6 +50,8 @@ var tagsInput = angular.module('ngTagsInput', []);
* @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
* @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
* @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
* @param {boolean=} [addOnPaste=false] Flag indicating that the text pasted into the input field will be split into tags.
* @param {string=} [pasteSplitPattern=,] Regular expression used to split the pasted text into tags.
* @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
* @param {string=} [allowedTagsPattern=.+] Regular expression that determines whether a new tag is valid.
* @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into
...
...
@@ -104,15 +60,17 @@ var tagsInput = angular.module('ngTagsInput', []);
* @param {boolean=} [addFromAutocompleteOnly=false] Flag indicating that only tags coming from the autocomplete list will be allowed.
* When this flag is true, addOnEnter, addOnComma, addOnSpace, addOnBlur and
* allowLeftoverText values are ignored.
* @param {boolean=} [spellcheck=true] Flag indicating whether the browser's spellcheck is enabled for the input field or not.
* @param {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag.
* @param {expression} onInvalidTag Expression to evaluate when a tag is invalid. The invalid tag is available as $tag.
* @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag.
*/
tagsInput
.
directive
(
'tagsInput'
,
[
"$timeout"
,
"$document"
,
"tagsInputConfig"
,
function
(
$timeout
,
$document
,
tagsInputConfig
)
{
tagsInput
.
directive
(
'tagsInput'
,
[
"$timeout"
,
"$document"
,
"tagsInputConfig"
,
"tiUtil"
,
function
(
$timeout
,
$document
,
tagsInputConfig
,
tiUtil
)
{
function
TagList
(
options
,
events
)
{
var
self
=
{},
getTagText
,
setTagText
,
tagIsValid
;
getTagText
=
function
(
tag
)
{
return
t
ag
[
options
.
displayProperty
]
;
return
t
iUtil
.
safeToString
(
tag
[
options
.
displayProperty
])
;
};
setTagText
=
function
(
tag
,
text
)
{
...
...
@@ -122,10 +80,11 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
tagIsValid
=
function
(
tag
)
{
var
tagText
=
getTagText
(
tag
);
return
tagText
.
length
>=
options
.
minLength
&&
tagText
.
length
<=
(
options
.
maxLength
||
tagText
.
length
)
&&
return
tagText
&&
tagText
.
length
>=
options
.
minLength
&&
tagText
.
length
<=
options
.
maxLength
&&
options
.
allowedTagsPattern
.
test
(
tagText
)
&&
!
findInObjectArray
(
self
.
items
,
tag
,
options
.
displayProperty
);
!
tiUtil
.
findInObjectArray
(
self
.
items
,
tag
,
options
.
displayProperty
);
};
self
.
items
=
[];
...
...
@@ -137,7 +96,7 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
};
self
.
add
=
function
(
tag
)
{
var
tagText
=
getTagText
(
tag
)
.
trim
()
;
var
tagText
=
getTagText
(
tag
);
if
(
options
.
replaceSpacesWithDashes
)
{
tagText
=
tagText
.
replace
(
/
\s
/g
,
'-'
);
...
...
@@ -149,7 +108,7 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
self
.
items
.
push
(
tag
);
events
.
trigger
(
'tag-added'
,
{
$tag
:
tag
});
}
else
{
else
if
(
tagText
)
{
events
.
trigger
(
'invalid-tag'
,
{
$tag
:
tag
});
}
...
...
@@ -179,46 +138,53 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
return
self
;
}
function
validateType
(
type
)
{
return
SUPPORTED_INPUT_TYPES
.
indexOf
(
type
)
!==
-
1
;
}
return
{
restrict
:
'E'
,
require
:
'ngModel'
,
scope
:
{
tags
:
'=ngModel'
,
onTagAdded
:
'&'
,
onInvalidTag
:
'&'
,
onTagRemoved
:
'&'
},
replace
:
false
,
transclude
:
true
,
templateUrl
:
'ngTagsInput/tags-input.html'
,
controller
:
[
"$scope"
,
"$attrs"
,
"$element"
,
function
(
$scope
,
$attrs
,
$element
)
{
$scope
.
events
=
tiUtil
.
simplePubSub
();
tagsInputConfig
.
load
(
'tagsInput'
,
$scope
,
$attrs
,
{
type
:
[
String
,
'text'
,
validateType
],
placeholder
:
[
String
,
'Add a tag'
],
tabindex
:
[
Number
],
tabindex
:
[
Number
,
null
],
removeTagSymbol
:
[
String
,
String
.
fromCharCode
(
215
)],
replaceSpacesWithDashes
:
[
Boolean
,
true
],
minLength
:
[
Number
,
3
],
maxLength
:
[
Number
],
maxLength
:
[
Number
,
MAX_SAFE_INTEGER
],
addOnEnter
:
[
Boolean
,
true
],
addOnSpace
:
[
Boolean
,
false
],
addOnComma
:
[
Boolean
,
true
],
addOnBlur
:
[
Boolean
,
true
],
addOnPaste
:
[
Boolean
,
false
],
pasteSplitPattern
:
[
RegExp
,
/,/
],
allowedTagsPattern
:
[
RegExp
,
/.+/
],
enableEditingLastTag
:
[
Boolean
,
false
],
minTags
:
[
Number
],
maxTags
:
[
Number
],
minTags
:
[
Number
,
0
],
maxTags
:
[
Number
,
MAX_SAFE_INTEGER
],
displayProperty
:
[
String
,
'text'
],
allowLeftoverText
:
[
Boolean
,
false
],
addFromAutocompleteOnly
:
[
Boolean
,
false
]
addFromAutocompleteOnly
:
[
Boolean
,
false
],
spellcheck
:
[
Boolean
,
true
]
});
$scope
.
events
=
new
SimplePubSub
();
$scope
.
tagList
=
new
TagList
(
$scope
.
options
,
$scope
.
events
);
this
.
registerAutocomplete
=
function
()
{
var
input
=
$element
.
find
(
'input'
);
input
.
on
(
'keydown'
,
function
(
e
)
{
$scope
.
events
.
trigger
(
'input-keydown'
,
e
);
});
return
{
addTag
:
function
(
tag
)
{
...
...
@@ -230,6 +196,9 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
getTags
:
function
()
{
return
$scope
.
tags
;
},
getCurrentTagText
:
function
()
{
return
$scope
.
newTag
.
text
;
},
getOptions
:
function
()
{
return
$scope
.
options
;
},
...
...
@@ -245,72 +214,119 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
tagList
=
scope
.
tagList
,
events
=
scope
.
events
,
options
=
scope
.
options
,
input
=
element
.
find
(
'input'
);
input
=
element
.
find
(
'input'
),
validationOptions
=
[
'minTags'
,
'maxTags'
,
'allowLeftoverText'
],
setElementValidity
;
setElementValidity
=
function
()
{
ngModelCtrl
.
$setValidity
(
'maxTags'
,
scope
.
tags
.
length
<=
options
.
maxTags
);
ngModelCtrl
.
$setValidity
(
'minTags'
,
scope
.
tags
.
length
>=
options
.
minTags
);
ngModelCtrl
.
$setValidity
(
'leftoverText'
,
options
.
allowLeftoverText
?
true
:
!
scope
.
newTag
.
text
);
};
scope
.
newTag
=
{
text
:
''
,
invalid
:
null
,
setText
:
function
(
value
)
{
this
.
text
=
value
;
events
.
trigger
(
'input-change'
,
value
);
}
};
scope
.
getDisplayText
=
function
(
tag
)
{
return
tiUtil
.
safeToString
(
tag
[
options
.
displayProperty
]);
};
scope
.
track
=
function
(
tag
)
{
return
tag
[
options
.
displayProperty
];
};
scope
.
$watch
(
'tags'
,
function
(
value
)
{
scope
.
tags
=
tiUtil
.
makeObjectArray
(
value
,
options
.
displayProperty
);
tagList
.
items
=
scope
.
tags
;
});
scope
.
$watch
(
'tags.length'
,
function
()
{
setElementValidity
();
});
scope
.
eventHandlers
=
{
input
:
{
change
:
function
(
text
)
{
events
.
trigger
(
'input-change'
,
text
);
},
keydown
:
function
(
$event
)
{
events
.
trigger
(
'input-keydown'
,
$event
);
},
focus
:
function
()
{
if
(
scope
.
hasFocus
)
{
return
;
}
scope
.
hasFocus
=
true
;
events
.
trigger
(
'input-focus'
);
},
blur
:
function
()
{
$timeout
(
function
()
{
var
activeElement
=
$document
.
prop
(
'activeElement'
),
lostFocusToBrowserWindow
=
activeElement
===
input
[
0
],
lostFocusToChildElement
=
element
[
0
].
contains
(
activeElement
);
if
(
lostFocusToBrowserWindow
||
!
lostFocusToChildElement
)
{
scope
.
hasFocus
=
false
;
events
.
trigger
(
'input-blur'
);
}
});
},
paste
:
function
(
$event
)
{
events
.
trigger
(
'input-paste'
,
$event
);
}
},
host
:
{
click
:
function
()
{
input
[
0
].
focus
();
}
}
};
events
.
on
(
'tag-added'
,
scope
.
onTagAdded
)
.
on
(
'invalid-tag'
,
scope
.
onInvalidTag
)
.
on
(
'tag-removed'
,
scope
.
onTagRemoved
)
.
on
(
'tag-added'
,
function
()
{
scope
.
newTag
.
text
=
''
;
scope
.
newTag
.
setText
(
''
)
;
})
.
on
(
'tag-added tag-removed'
,
function
()
{
// Sets the element to its dirty state
// In Angular 1.3 this will be replaced with $setDirty.
ngModelCtrl
.
$setViewValue
(
scope
.
tags
);
})
.
on
(
'invalid-tag'
,
function
()
{
scope
.
newTag
.
invalid
=
true
;
})
.
on
(
'option-change'
,
function
(
e
)
{
if
(
validationOptions
.
indexOf
(
e
.
name
)
!==
-
1
)
{
setElementValidity
();
}
})
.
on
(
'input-change'
,
function
()
{
tagList
.
selected
=
null
;
scope
.
newTag
.
invalid
=
null
;
})
.
on
(
'input-focus'
,
function
()
{
element
.
triggerHandler
(
'focus'
);
ngModelCtrl
.
$setValidity
(
'leftoverText'
,
true
);
})
.
on
(
'input-blur'
,
function
()
{
if
(
!
options
.
addFromAutocompleteOnly
)
{
if
(
options
.
addOnBlur
)
{
if
(
options
.
addOnBlur
&&
!
options
.
addFromAutocompleteOnly
)
{
tagList
.
addText
(
scope
.
newTag
.
text
);
}
ngModelCtrl
.
$setValidity
(
'leftoverText'
,
options
.
allowLeftoverText
?
true
:
!
scope
.
newTag
.
text
);
}
});
scope
.
newTag
=
{
text
:
''
,
invalid
:
null
};
scope
.
getDisplayText
=
function
(
tag
)
{
return
tag
[
options
.
displayProperty
].
trim
();
};
scope
.
track
=
function
(
tag
)
{
return
tag
[
options
.
displayProperty
];
};
scope
.
newTagChange
=
function
()
{
events
.
trigger
(
'input-change'
,
scope
.
newTag
.
text
);
};
scope
.
$watch
(
'tags'
,
function
(
value
)
{
scope
.
tags
=
makeObjectArray
(
value
,
options
.
displayProperty
);
tagList
.
items
=
scope
.
tags
;
});
scope
.
$watch
(
'tags.length'
,
function
(
value
)
{
ngModelCtrl
.
$setValidity
(
'maxTags'
,
angular
.
isUndefined
(
options
.
maxTags
)
||
value
<=
options
.
maxTags
);
ngModelCtrl
.
$setValidity
(
'minTags'
,
angular
.
isUndefined
(
options
.
minTags
)
||
value
>=
options
.
minTags
);
});
input
.
on
(
'keydown'
,
function
(
e
)
{
// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
if
(
e
.
isImmediatePropagationStopped
&&
e
.
isImmediatePropagationStopped
())
{
return
;
}
var
key
=
e
.
keyCode
,
isModifier
=
e
.
shiftKey
||
e
.
altKey
||
e
.
ctrlKey
||
e
.
metaKey
,
element
.
triggerHandler
(
'blur'
);
setElementValidity
();
})
.
on
(
'input-keydown'
,
function
(
event
)
{
var
key
=
event
.
keyCode
,
isModifier
=
event
.
shiftKey
||
event
.
altKey
||
event
.
ctrlKey
||
event
.
metaKey
,
addKeys
=
{},
shouldAdd
,
shouldRemove
;
...
...
@@ -327,49 +343,34 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
if
(
shouldAdd
)
{
tagList
.
addText
(
scope
.
newTag
.
text
);
scope
.
$apply
();
e
.
preventDefault
();
event
.
preventDefault
();
}
else
if
(
shouldRemove
)
{
var
tag
=
tagList
.
removeLast
();
if
(
tag
&&
options
.
enableEditingLastTag
)
{
scope
.
newTag
.
text
=
tag
[
options
.
displayProperty
]
;
scope
.
newTag
.
setText
(
tag
[
options
.
displayProperty
])
;
}
scope
.
$apply
();
e
.
preventDefault
();
event
.
preventDefault
();
}
})
.
on
(
'focus'
,
function
()
{
if
(
scope
.
hasFocus
)
{
return
;
.
on
(
'input-paste'
,
function
(
event
)
{
if
(
options
.
addOnPaste
)
{
var
data
=
event
.
clipboardData
.
getData
(
'text/plain'
);
var
tags
=
data
.
split
(
options
.
pasteSplitPattern
);
if
(
tags
.
length
>
1
)
{
tags
.
forEach
(
function
(
tag
)
{
tagList
.
addText
(
tag
);
});
event
.
preventDefault
();
}
scope
.
hasFocus
=
true
;
events
.
trigger
(
'input-focus'
);
scope
.
$apply
();
})
.
on
(
'blur'
,
function
()
{
$timeout
(
function
()
{
var
activeElement
=
$document
.
prop
(
'activeElement'
),
lostFocusToBrowserWindow
=
activeElement
===
input
[
0
],
lostFocusToChildElement
=
element
[
0
].
contains
(
activeElement
);
if
(
lostFocusToBrowserWindow
||
!
lostFocusToChildElement
)
{
scope
.
hasFocus
=
false
;
events
.
trigger
(
'input-blur'
);
}
});
});
element
.
find
(
'div'
).
on
(
'click'
,
function
()
{
input
[
0
].
focus
();
});
}
};
}]);
/**
* @ngdoc directive
* @name autoComplete
...
...
@@ -388,14 +389,23 @@ tagsInput.directive('tagsInput', ["$timeout","$document","tagsInputConfig", func
* @param {boolean=} [highlightMatchedText=true] Flag indicating that the matched text will be highlighted in the
* suggestions list.
* @param {number=} [maxResultsToShow=10] Maximum number of results to be displayed at a time.
* @param {boolean=} [loadOnDownArrow=false] Flag indicating that the source option will be evaluated when the down arrow
* key is pressed and the suggestion list is closed. The current input value
* is available as $query.
* @param {boolean=} {loadOnEmpty=false} Flag indicating that the source option will be evaluated when the input content
* becomes empty. The $query variable will be passed to the expression as an empty string.
* @param {boolean=} {loadOnFocus=false} Flag indicating that the source option will be evaluated when the input element
* gains focus. The current input value is available as $query.
* @param {boolean=} [selectFirstMatch=true] Flag indicating that the first match will be automatically selected once
* the suggestion list is shown.
*/
tagsInput
.
directive
(
'autoComplete'
,
[
"$document"
,
"$timeout"
,
"$sce"
,
"
tagsInputConfig"
,
function
(
$document
,
$timeout
,
$sce
,
tagsInputConfig
)
{
tagsInput
.
directive
(
'autoComplete'
,
[
"$document"
,
"$timeout"
,
"$sce"
,
"
$q"
,
"tagsInputConfig"
,
"tiUtil"
,
function
(
$document
,
$timeout
,
$sce
,
$q
,
tagsInputConfig
,
tiUtil
)
{
function
SuggestionList
(
loadFn
,
options
)
{
var
self
=
{},
debouncedLoadId
,
getDifference
,
lastPromise
;
var
self
=
{},
getDifference
,
lastPromise
;
getDifference
=
function
(
array1
,
array2
)
{
return
array1
.
filter
(
function
(
item
)
{
return
!
findInObjectArray
(
array2
,
item
,
options
.
tagsInput
.
displayProperty
);
return
!
tiUtil
.
findInObjectArray
(
array2
,
item
,
options
.
tagsInput
.
displayProperty
);
});
};
...
...
@@ -407,24 +417,20 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
self
.
index
=
-
1
;
self
.
selected
=
null
;
self
.
query
=
null
;
$timeout
.
cancel
(
debouncedLoadId
);
};
self
.
show
=
function
()
{
if
(
options
.
selectFirstMatch
)
{
self
.
select
(
0
);
}
else
{
self
.
selected
=
null
;
}
self
.
visible
=
true
;
};
self
.
load
=
function
(
query
,
tags
)
{
if
(
query
.
length
<
options
.
minLength
)
{
self
.
reset
();
return
;
}
$timeout
.
cancel
(
debouncedLoadId
);
debouncedLoadId
=
$timeout
(
function
()
{
self
.
load
=
tiUtil
.
debounce
(
function
(
query
,
tags
)
{
self
.
query
=
query
;
var
promise
=
loadFn
({
$query
:
query
}
);
var
promise
=
$q
.
when
(
loadFn
({
$query
:
query
})
);
lastPromise
=
promise
;
promise
.
then
(
function
(
items
)
{
...
...
@@ -432,7 +438,7 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
return
;
}
items
=
makeObjectArray
(
items
.
data
||
items
,
options
.
tagsInput
.
displayProperty
);
items
=
tiUtil
.
makeObjectArray
(
items
.
data
||
items
,
options
.
tagsInput
.
displayProperty
);
items
=
getDifference
(
items
,
tags
);
self
.
items
=
items
.
slice
(
0
,
options
.
maxResultsToShow
);
...
...
@@ -443,8 +449,8 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
self
.
reset
();
}
});
},
options
.
debounceDelay
,
false
);
};
},
options
.
debounceDelay
);
self
.
selectNext
=
function
()
{
self
.
select
(
++
self
.
index
);
};
...
...
@@ -467,12 +473,6 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
return
self
;
}
function
encodeHTML
(
value
)
{
return
value
.
replace
(
/&/g
,
'&'
)
.
replace
(
/</g
,
'<'
)
.
replace
(
/>/g
,
'>'
);
}
return
{
restrict
:
'E'
,
require
:
'^tagsInput'
,
...
...
@@ -480,13 +480,17 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
templateUrl
:
'ngTagsInput/auto-complete.html'
,
link
:
function
(
scope
,
element
,
attrs
,
tagsInputCtrl
)
{
var
hotkeys
=
[
KEYS
.
enter
,
KEYS
.
tab
,
KEYS
.
escape
,
KEYS
.
up
,
KEYS
.
down
],
suggestionList
,
tagsInput
,
options
,
getItem
Text
,
documentClick
;
suggestionList
,
tagsInput
,
options
,
getItem
,
getDisplayText
,
shouldLoadSuggestions
;
tagsInputConfig
.
load
(
'autoComplete'
,
scope
,
attrs
,
{
debounceDelay
:
[
Number
,
100
],
minLength
:
[
Number
,
3
],
highlightMatchedText
:
[
Boolean
,
true
],
maxResultsToShow
:
[
Number
,
10
]
maxResultsToShow
:
[
Number
,
10
],
loadOnDownArrow
:
[
Boolean
,
false
],
loadOnEmpty
:
[
Boolean
,
false
],
loadOnFocus
:
[
Boolean
,
false
],
selectFirstMatch
:
[
Boolean
,
true
]
});
options
=
scope
.
options
;
...
...
@@ -496,12 +500,25 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
suggestionList
=
new
SuggestionList
(
scope
.
source
,
options
);
getItem
Text
=
function
(
item
)
{
getItem
=
function
(
item
)
{
return
item
[
options
.
tagsInput
.
displayProperty
];
};
getDisplayText
=
function
(
item
)
{
return
tiUtil
.
safeToString
(
getItem
(
item
));
};
shouldLoadSuggestions
=
function
(
value
)
{
return
value
&&
value
.
length
>=
options
.
minLength
||
!
value
&&
options
.
loadOnEmpty
;
};
scope
.
suggestionList
=
suggestionList
;
scope
.
addSuggestionByIndex
=
function
(
index
)
{
suggestionList
.
select
(
index
);
scope
.
addSuggestion
();
};
scope
.
addSuggestion
=
function
()
{
var
added
=
false
;
...
...
@@ -516,51 +533,45 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
};
scope
.
highlight
=
function
(
item
)
{
var
text
=
get
Item
Text
(
item
);
text
=
encodeHTML
(
text
);
var
text
=
get
Display
Text
(
item
);
text
=
tiUtil
.
encodeHTML
(
text
);
if
(
options
.
highlightMatchedText
)
{
text
=
replaceAll
(
text
,
encodeHTML
(
suggestionList
.
query
),
'<em>$&</em>'
);
text
=
tiUtil
.
safeHighlight
(
text
,
tiUtil
.
encodeHTML
(
suggestionList
.
query
)
);
}
return
$sce
.
trustAsHtml
(
text
);
};
scope
.
track
=
function
(
item
)
{
return
getItem
Text
(
item
);
return
getItem
(
item
);
};
tagsInput
.
on
(
'tag-added invalid-tag'
,
function
()
{
.
on
(
'tag-added invalid-tag
input-blur
'
,
function
()
{
suggestionList
.
reset
();
})
.
on
(
'input-change'
,
function
(
value
)
{
if
(
value
)
{
if
(
shouldLoadSuggestions
(
value
)
)
{
suggestionList
.
load
(
value
,
tagsInput
.
getTags
());
}
else
{
}
else
{
suggestionList
.
reset
();
}
})
.
on
(
'input-keydown'
,
function
(
e
)
{
var
key
,
handled
;
.
on
(
'input-focus'
,
function
()
{
var
value
=
tagsInput
.
getCurrentTagText
();
if
(
options
.
loadOnFocus
&&
shouldLoadSuggestions
(
value
))
{
suggestionList
.
load
(
value
,
tagsInput
.
getTags
());
}
})
.
on
(
'input-keydown'
,
function
(
event
)
{
var
key
=
event
.
keyCode
,
handled
=
false
;
if
(
hotkeys
.
indexOf
(
e
.
keyCode
)
===
-
1
)
{
if
(
hotkeys
.
indexOf
(
key
)
===
-
1
)
{
return
;
}
// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
var
immediatePropagationStopped
=
false
;
e
.
stopImmediatePropagation
=
function
()
{
immediatePropagationStopped
=
true
;
e
.
stopPropagation
();
};
e
.
isImmediatePropagationStopped
=
function
()
{
return
immediatePropagationStopped
;
};
if
(
suggestionList
.
visible
)
{
key
=
e
.
keyCode
;
handled
=
false
;
if
(
key
===
KEYS
.
down
)
{
suggestionList
.
selectNext
();
...
...
@@ -577,34 +588,25 @@ tagsInput.directive('autoComplete', ["$document","$timeout","$sce","tagsInputCon
else
if
(
key
===
KEYS
.
enter
||
key
===
KEYS
.
tab
)
{
handled
=
scope
.
addSuggestion
();
}
if
(
handled
)
{
e
.
preventDefault
();
e
.
stopImmediatePropagation
();
scope
.
$apply
();
}
else
{
if
(
key
===
KEYS
.
down
&&
scope
.
options
.
loadOnDownArrow
)
{
suggestionList
.
load
(
tagsInput
.
getCurrentTagText
(),
tagsInput
.
getTags
());
handled
=
true
;
}
})
.
on
(
'input-blur'
,
function
()
{
suggestionList
.
reset
();
});
documentClick
=
function
()
{
if
(
suggestionList
.
visible
)
{
suggestionList
.
reset
();
scope
.
$apply
();
}
};
$document
.
on
(
'click'
,
documentClick
);
scope
.
$on
(
'$destroy'
,
function
()
{
$document
.
off
(
'click'
,
documentClick
);
if
(
handled
)
{
event
.
preventDefault
();
event
.
stopImmediatePropagation
();
return
false
;
}
});
}
};
}]);
/**
* @ngdoc directive
* @name tiTranscludeAppend
...
...
@@ -629,12 +631,12 @@ tagsInput.directive('tiTranscludeAppend', function() {
* @description
* Automatically sets the input's width so its content is always visible. Used internally by tagsInput directive.
*/
tagsInput
.
directive
(
'tiAutosize'
,
function
(
)
{
tagsInput
.
directive
(
'tiAutosize'
,
[
"tagsInputConfig"
,
function
(
tagsInputConfig
)
{
return
{
restrict
:
'A'
,
require
:
'ngModel'
,
link
:
function
(
scope
,
element
,
attrs
,
ctrl
)
{
var
THRESHOLD
=
3
,
var
threshold
=
tagsInputConfig
.
getTextAutosizeThreshold
()
,
span
,
resize
;
span
=
angular
.
element
(
'<span class="input"></span>'
);
...
...
@@ -659,7 +661,7 @@ tagsInput.directive('tiAutosize', function() {
span
.
css
(
'display'
,
'none'
);
}
element
.
css
(
'width'
,
width
?
width
+
THRESHOLD
+
'px'
:
''
);
element
.
css
(
'width'
,
width
?
width
+
threshold
+
'px'
:
''
);
return
originalValue
;
};
...
...
@@ -674,6 +676,24 @@ tagsInput.directive('tiAutosize', function() {
});
}
};
}]);
/**
* @ngdoc directive
* @name tiBindAttrs
* @module ngTagsInput
*
* @description
* Binds attributes to expressions. Used internally by tagsInput directive.
*/
tagsInput
.
directive
(
'tiBindAttrs'
,
function
()
{
return
function
(
scope
,
element
,
attrs
)
{
scope
.
$watch
(
attrs
.
tiBindAttrs
,
function
(
value
)
{
angular
.
forEach
(
value
,
function
(
value
,
key
)
{
attrs
.
$set
(
key
,
value
);
});
},
true
);
};
});
/**
...
...
@@ -686,7 +706,9 @@ tagsInput.directive('tiAutosize', function() {
* initialize options from HTML attributes.
*/
tagsInput
.
provider
(
'tagsInputConfig'
,
function
()
{
var
globalDefaults
=
{},
interpolationStatus
=
{};
var
globalDefaults
=
{},
interpolationStatus
=
{},
autosizeThreshold
=
3
;
/**
* @ngdoc method
...
...
@@ -720,6 +742,20 @@ tagsInput.provider('tagsInputConfig', function() {
return
this
;
};
/***
* @ngdoc method
* @name setTextAutosizeThreshold
* @methodOf tagsInputConfig
*
* @param {number} threshold Threshold to be used by the tagsInput directive to re-size the input element based on its contents.
*
* @returns {object} The service itself for chaining purposes.
*/
this
.
setTextAutosizeThreshold
=
function
(
threshold
)
{
autosizeThreshold
=
threshold
;
return
this
;
};
this
.
$get
=
[
"$interpolate"
,
function
(
$interpolate
)
{
var
converters
=
{};
converters
[
String
]
=
function
(
value
)
{
return
value
;
};
...
...
@@ -729,13 +765,16 @@ tagsInput.provider('tagsInputConfig', function() {
return
{
load
:
function
(
directive
,
scope
,
attrs
,
options
)
{
var
defaultValidator
=
function
()
{
return
true
;
};
scope
.
options
=
{};
angular
.
forEach
(
options
,
function
(
value
,
key
)
{
var
type
,
localDefault
,
converter
,
getDefault
,
updateValue
;
var
type
,
localDefault
,
validator
,
converter
,
getDefault
,
updateValue
;
type
=
value
[
0
];
localDefault
=
value
[
1
];
validator
=
value
[
2
]
||
defaultValidator
;
converter
=
converters
[
type
];
getDefault
=
function
()
{
...
...
@@ -744,32 +783,131 @@ tagsInput.provider('tagsInputConfig', function() {
};
updateValue
=
function
(
value
)
{
scope
.
options
[
key
]
=
value
?
converter
(
value
)
:
getDefault
();
scope
.
options
[
key
]
=
value
&&
validator
(
value
)
?
converter
(
value
)
:
getDefault
();
};
if
(
interpolationStatus
[
directive
]
&&
interpolationStatus
[
directive
][
key
])
{
attrs
.
$observe
(
key
,
function
(
value
)
{
updateValue
(
value
);
scope
.
events
.
trigger
(
'option-change'
,
{
name
:
key
,
newValue
:
value
});
});
}
else
{
updateValue
(
attrs
[
key
]
&&
$interpolate
(
attrs
[
key
])(
scope
.
$parent
));
}
});
},
getTextAutosizeThreshold
:
function
()
{
return
autosizeThreshold
;
}
};
}];
});
/***
* @ngdoc factory
* @name tiUtil
* @module ngTagsInput
*
* @description
* Helper methods used internally by the directive. Should not be used directly from user code.
*/
tagsInput
.
factory
(
'tiUtil'
,
[
"$timeout"
,
function
(
$timeout
)
{
var
self
=
{};
self
.
debounce
=
function
(
fn
,
delay
)
{
var
timeoutId
;
return
function
()
{
var
args
=
arguments
;
$timeout
.
cancel
(
timeoutId
);
timeoutId
=
$timeout
(
function
()
{
fn
.
apply
(
null
,
args
);
},
delay
);
};
};
self
.
makeObjectArray
=
function
(
array
,
key
)
{
array
=
array
||
[];
if
(
array
.
length
>
0
&&
!
angular
.
isObject
(
array
[
0
]))
{
array
.
forEach
(
function
(
item
,
index
)
{
array
[
index
]
=
{};
array
[
index
][
key
]
=
item
;
});
}
return
array
;
};
self
.
findInObjectArray
=
function
(
array
,
obj
,
key
)
{
var
item
=
null
;
for
(
var
i
=
0
;
i
<
array
.
length
;
i
++
)
{
// I'm aware of the internationalization issues regarding toLowerCase()
// but I couldn't come up with a better solution right now
if
(
self
.
safeToString
(
array
[
i
][
key
]).
toLowerCase
()
===
self
.
safeToString
(
obj
[
key
]).
toLowerCase
())
{
item
=
array
[
i
];
break
;
}
}
return
item
;
};
self
.
safeHighlight
=
function
(
str
,
value
)
{
if
(
!
value
)
{
return
str
;
}
function
escapeRegexChars
(
str
)
{
return
str
.
replace
(
/
([
.?*+^$[
\]\\
(){}|-
])
/g
,
'
\\
$1'
);
}
var
expression
=
new
RegExp
(
'&[^;]+;|'
+
escapeRegexChars
(
value
),
'gi'
);
return
str
.
replace
(
expression
,
function
(
match
)
{
return
match
===
value
?
'<em>'
+
value
+
'</em>'
:
match
;
});
};
self
.
safeToString
=
function
(
value
)
{
return
angular
.
isUndefined
(
value
)
||
value
==
null
?
''
:
value
.
toString
().
trim
();
};
self
.
encodeHTML
=
function
(
value
)
{
return
value
.
replace
(
/&/g
,
'&'
)
.
replace
(
/</g
,
'<'
)
.
replace
(
/>/g
,
'>'
);
};
self
.
simplePubSub
=
function
()
{
var
events
=
{};
return
{
on
:
function
(
names
,
handler
)
{
names
.
split
(
' '
).
forEach
(
function
(
name
)
{
if
(
!
events
[
name
])
{
events
[
name
]
=
[];
}
events
[
name
].
push
(
handler
);
});
return
this
;
},
trigger
:
function
(
name
,
args
)
{
var
handlers
=
events
[
name
]
||
[];
handlers
.
every
(
function
(
handler
)
{
var
retVal
=
handler
.
call
(
null
,
args
);
return
angular
.
isUndefined
(
retVal
)
||
retVal
;
});
return
this
;
}
};
};
return
self
;
}]);
/* HTML templates */
tagsInput
.
run
([
"$templateCache"
,
function
(
$templateCache
)
{
$templateCache
.
put
(
'ngTagsInput/tags-input.html'
,
"<div class=
\"
host
\"
tabindex=
\"
-1
\"
ti-transclude-append=
\"\"
><div class=
\"
tags
\"
ng-class=
\"
{focused: hasFocus}
\"
><ul class=
\"
tag-list
\"
><li class=
\"
tag-item
\"
ng-repeat=
\"
tag in tagList.items track by track(tag)
\"
ng-class=
\"
{ selected: tag == tagList.selected }
\"
><span>{{getDisplayText(tag)}}</span> <a class=
\"
remove-button
\"
ng-click=
\"
tagList.remove($index)
\"
>{{options.removeTagSymbol}}</a></li></ul><input class=
\"
input
\"
placeholder=
\"
{{options.placeholder}}
\"
tabindex=
\"
{{options.tabindex}}
\"
ng-model=
\"
newTag.text
\"
ng-change=
\"
newTagChange()
\"
ng-trim=
\"
false
\"
ng-class=
\"
{'invalid-tag': newTag.invalid
}
\"
ti-autosize=
\"\"
></div></div>"
"<div class=
\"
host
\"
tabindex=
\"
-1
\"
ng-click=
\"
eventHandlers.host.click()
\"
ti-transclude-append=
\"\"
><div class=
\"
tags
\"
ng-class=
\"
{focused: hasFocus}
\"
><ul class=
\"
tag-list
\"
><li class=
\"
tag-item
\"
ng-repeat=
\"
tag in tagList.items track by track(tag)
\"
ng-class=
\"
{ selected: tag == tagList.selected }
\"
><span ng-bind=
\"
getDisplayText(tag)
\"
></span> <a class=
\"
remove-button
\"
ng-click=
\"
tagList.remove($index)
\"
ng-bind=
\"
options.removeTagSymbol
\"
></a></li></ul><input class=
\"
input
\"
ng-model=
\"
newTag.text
\"
ng-change=
\"
eventHandlers.input.change(newTag.text)
\"
ng-keydown=
\"
eventHandlers.input.keydown($event)
\"
ng-focus=
\"
eventHandlers.input.focus($event)
\"
ng-blur=
\"
eventHandlers.input.blur($event)
\"
ng-paste=
\"
eventHandlers.input.paste($event)
\"
ng-trim=
\"
false
\"
ng-class=
\"
{'invalid-tag': newTag.invalid}
\"
ti-bind-attrs=
\"
{type: options.type, placeholder: options.placeholder, tabindex: options.tabindex, spellcheck: options.spellcheck
}
\"
ti-autosize=
\"\"
></div></div>"
);
$templateCache
.
put
(
'ngTagsInput/auto-complete.html'
,
"<div class=
\"
autocomplete
\"
ng-show=
\"
suggestionList.visible
\"
><ul class=
\"
suggestion-list
\"
><li class=
\"
suggestion-item
\"
ng-repeat=
\"
item in suggestionList.items track by track(item)
\"
ng-class=
\"
{selected: item == suggestionList.selected}
\"
ng-click=
\"
addSuggestion
(
)
\"
ng-mouseenter=
\"
suggestionList.select($index)
\"
ng-bind-html=
\"
highlight(item)
\"
></li></ul></div>"
"<div class=
\"
autocomplete
\"
ng-show=
\"
suggestionList.visible
\"
><ul class=
\"
suggestion-list
\"
><li class=
\"
suggestion-item
\"
ng-repeat=
\"
item in suggestionList.items track by track(item)
\"
ng-class=
\"
{selected: item == suggestionList.selected}
\"
ng-click=
\"
addSuggestion
ByIndex($index
)
\"
ng-mouseenter=
\"
suggestionList.select($index)
\"
ng-bind-html=
\"
highlight(item)
\"
></li></ul></div>"
);
}]);
...
...
h/static/styles/tags-input.scss
View file @
88af141c
...
...
@@ -3,6 +3,7 @@
@import
"mixins/forms"
;
@import
"compass/css3/user-interface"
;
@import
"variables"
;
tags-input
{
.host
{
...
...
@@ -101,3 +102,49 @@ tags-input {
}
}
tags-input
.autocomplete
{
margin-top
:
.3em
;
position
:
absolute
;
padding
:
.3em
0
;
z-index
:
999
;
width
:
100%
;
background-color
:
white
;
border
:
thin
solid
rgba
(
0
,
0
,
0
,
0
.2
);
-webkit-box-shadow
:
0
.3em
.6em
rgba
(
0
,
0
,
0
,
0
.2
);
-moz-box-shadow
:
0
.3em
.6em
rgba
(
0
,
0
,
0
,
0
.2
);
box-shadow
:
0
.3em
.6em
rgba
(
0
,
0
,
0
,
0
.2
);
.suggestion-list
{
margin
:
0
;
padding
:
0
;
list-style-type
:
none
;
}
.suggestion-item
{
padding
:
.3em
.6em
;
cursor
:
pointer
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
font-family
:
$sans-font-family
;
color
:
black
;
background-color
:
white
;
em
{
font-family
:
$sans-font-family
;
font-weight
:
bold
;
font-style
:
normal
;
color
:
black
;
background-color
:
white
;
}
&
.selected
{
color
:
white
;
background-color
:
#0097cf
;
em
{
color
:
white
;
background-color
:
#0097cf
;
}
}
}
}
h/templates/client/annotation.html
View file @
88af141c
...
...
@@ -80,7 +80,11 @@
placeholder=
"Add tags…"
min-length=
"1"
replace-spaces-with-dashes=
"false"
enable-editing-last-tag=
"true"
></tags-input>
enable-editing-last-tag=
"true"
>
<auto-complete
source=
"vm.tagsAutoComplete($query)"
min-length=
"1"
max-results-to-show=
"10"
></auto-complete>
</tags-input>
</div>
<div
class=
"annotation-section tags tags-read-only"
...
...
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