Skip to content

Instantly share code, notes, and snippets.

@johnjansen
Last active March 1, 2017 21:11
Show Gist options
  • Save johnjansen/fbbf227694c5583138e84c9f6dd1aaae to your computer and use it in GitHub Desktop.
Save johnjansen/fbbf227694c5583138e84c9f6dd1aaae to your computer and use it in GitHub Desktop.
Coffeescript Tokenfield (with jQuery, step by step)
class window.Tokenfield
# create the object
constructor: (target)->
@properties=
target_object: $(target)
separator_key_codes: [9, 10, 12, 13] # maps to keyboard KEYS
separator_char_codes: [9, 10, 11, 12, 13, 44, 58, 59].map (i)-> String.fromCharCode(i) # characters (for paste)
del: [8]
min_input_width: 200 # default, can be overidden in css
token_list: []
# draw the control
draw: ->
@properties.new_content = $("<tokens></tokens>")
@properties.target_object.append(@properties.new_content)
@properties.add_field = $("<add contenteditable></add>")
@properties.target_object.append(@properties.add_field)
css_field_min_width = parseInt(@properties.add_field.css('min-width'))
if css_field_min_width > 0
@properties.min_input_width = parseInt(@properties.add_field.css('min-width'))
@properties.add_field[0].style.minWidth = "initial"
@properties.token_field = $("<input type='hidden' name='tokens'></input>")
@properties.target_object.append(@properties.token_field)
@bind_events()
this
redraw: ->
# clear all the tokens
@properties.new_content.html("")
@properties.new_content.removeClass("overflowed")
# BACKWARDS, create tokens until overflow
last = @properties.token_list.length
for i in [last-1..0]
tk = @new_token_tag(@properties.token_list[i])
@properties.new_content.prepend(tk)
if @properties.add_field.outerWidth() < @properties.min_input_width
tk.remove()
@properties.new_content.addClass("overflowed")
break
@update_token_field()
# handle pasted text
# refactor
paste: (text)->
paste_split_regex = new RegExp(@properties.separator_char_codes.join("|"), "gi")
that = this
text.split(paste_split_regex).map (i)->
that.new_token(i.trim())
@redraw()
update_token_field: ->
@properties.token_field.val(@properties.token_list.join(@properties.separator_char_codes[0]))
this
# delete the last tag
delete_last_token: ->
@properties.token_list.pop()
this
# delete a given token
delete_token: (token)->
token.remove()
this
# reset the input field
reset_input: ->
@properties.add_field.html("")
this
# create a new token in the interface
make_token: ->
if @typing()
@new_token()
this
# bind events to objects
bind_events: ->
that = this
@properties.add_field.bind "keydown", (e)->
return that.keydown(e)
@properties.add_field.bind "paste", (e)->
that.paste(e.originalEvent.clipboardData.getData("text"))
return false
# keydown
keydown: (key)->
if @backspace(key)
if @typing()
return true
else if @has_tokens()
@delete_last_token()
@redraw()
return false
else if @separator(key)
@make_token()
@reset_input()
@redraw()
return false
# new tag text
new_tag_text: ->
return @properties.add_field.text().trim()
# typing
typing: ->
return @new_tag_text().length > 0
# is the key a separator
separator: (key) ->
sep_char = @properties.separator_char_codes.indexOf(key.originalEvent.key) >= 0
sep_key = @properties.separator_key_codes.indexOf(key.which) >= 0
return sep_char || sep_key
# is the key a backspace
backspace: (key) ->
return @properties.del.indexOf(key.which) >= 0
# return all tokens
tokens: ->
return @properties.new_content.find("token")
# return bool indicating if there is at least 1 token
has_tokens: ->
return @tokens().length > 0
# return a tag for the new token
new_token: (text)->
if text is undefined
text = @new_tag_text()
if text.length > 0 && @properties.token_list.indexOf(text) == -1
@properties.token_list.push(text)
new_token_tag: (token)->
# add a token to the beginning of the tokens list
tk = $("<token>" + token + "</token>")
# # bind the click event
that = this
tk.click (e)->
# find the token (use the index)
last = that.properties.token_list.length
text = $(this).text()
for i in [last-1..0]
if that.properties.token_list[i] == text
that.properties.token_list.splice(i, 1)
break
that.redraw()
#tokenfield {
padding: 12px 10px 8px 10px;
display: flex;
width: 752px;
height: 34px;
border-radius: 8px;
background-color: #f9fbfd;
border: solid 1px #cfd8dc;
font-family: Roboto;
font-size: 18px;
font-weight: normal;
font-style: normal;
font-stretch: normal;
line-height: 1.78;
letter-spacing: normal;
color: #263238;
}
#tokenfield tokens {
float:left;
white-space: nowrap;
}
#tokenfield tokens.overflowed::before {
content: "\00AB \0020";
position: relative;
top: -10px;
}
#tokenfield tokens token {
padding: 0px 31px 0px 10px;
background-color: #fff;
margin-right: 10px;
cursor: hand;
cursor: pointer;
border-radius: 4px;
background-image: url("delete.svg");
background-position: right 10px center;
background-repeat: no-repeat;
height: 32px;
display: inline-block;
max-width: 200px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
#tokenfield add {
width: 100%;
padding: 0px;
margin-bottom: 2px;
cursor: text;
overflow: auto;
overflow-y: hidden;
white-space: nowrap;
min-width: 100px;
}
#tokenfield add:hover {
outline: none;
}
<html>
<head>
<link rel="stylesheet" media="screen" href="https://fonts.googleapis.com/css?family=Cormorant+Garamond|Lalezar|Roboto">
<link rel="stylesheet" media="screen" href="tokenfield.css">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/coffee-script/1.1.2/coffee-script.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js"></script>
<head>
<body>
<div id="tokenfield"></div>
<script type="text/javascript" src="tokenfield.js"></script>
<script type="text/coffeescript">
$ ->
window.tokenfield = new Tokenfield("#tokenfield")
window.tokenfield.draw()
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment