├── .Rbuildignore ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── R └── tagsTextInput.R ├── README.Rmd ├── README.md ├── gif └── fruits.gif ├── inst └── js │ ├── bootstrap-tagsinput.css │ └── bootstrap-tagsinput.js ├── man └── tagsTextInput.Rd └── tagsinput.Rproj /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^README-.*\.png$ 5 | gif 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: tagsinput 2 | Type: Package 3 | Title: Bootstrap Tags input for Shiny 4 | Version: 0.1.0 5 | Authors@R: c( 6 | person("Romain", "François", email = "romain@thinkr.fr", role = c("aut", "cre")), 7 | person("Dean", "Attali", email = "daattali@gmail.com", role = "ctb" ) 8 | ) 9 | Description: Bootstrap Tags input for Shiny. 10 | Encoding: UTF-8 11 | LazyData: true 12 | License: MIT + file LICENSE 13 | Imports: shiny, 14 | htmltools 15 | RoxygenNote: 6.0.1 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2017 2 | COPYRIGHT HOLDER: ThinkR 3 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(tagsTextInput) 4 | importFrom(htmltools,htmlDependency) 5 | importFrom(htmltools,tagAppendAttributes) 6 | importFrom(shiny,textInput) 7 | importFrom(utils,packageVersion) 8 | -------------------------------------------------------------------------------- /R/tagsTextInput.R: -------------------------------------------------------------------------------- 1 | 2 | #' @importFrom htmltools htmlDependency 3 | #' @importFrom utils packageVersion 4 | #' @noRd 5 | tagsInputDependencies <- function(){ 6 | version <- as.character( packageVersion("tagsinput")[[1]] ) 7 | src <- system.file( "js", package = "tagsinput" ) 8 | dep <- htmlDependency( 9 | name = "tagsinput", 10 | version = version, 11 | src = src, 12 | script = "bootstrap-tagsinput.js", 13 | stylesheet = "bootstrap-tagsinput.css" 14 | ) 15 | list( dep ) 16 | } 17 | 18 | 19 | #' text input specific to tags 20 | #' 21 | #' @param ... see \code{\link[shiny]{textInput}} 22 | #' 23 | #' @importFrom shiny textInput 24 | #' @importFrom htmltools tagAppendAttributes 25 | #' @export 26 | #' 27 | #' @examples 28 | #' \dontrun{ 29 | #' library(shiny) 30 | #' ui <- fluidPage( 31 | #' tagsTextInput("fruits", "Fruits", "apple, banana"), 32 | #' textOutput("out") 33 | #' ) 34 | #' 35 | #' server <- function(input, output){ 36 | #' output$out <- renderPrint( strsplit( input$fruits, ",")[[1]] ) 37 | #' } 38 | #' 39 | #' shinyApp( ui, server ) 40 | #' 41 | #' } 42 | tagsTextInput <- function(...) { 43 | res <- textInput(...) 44 | res$children[[2]] <- tagAppendAttributes( res$children[[2]], `data-role` = "tagsinput" ) 45 | attr(res, "html_dependencies") <- tagsInputDependencies() 46 | res 47 | } 48 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, echo = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "README-" 12 | ) 13 | ``` 14 | 15 | ```{r, eval=FALSE} 16 | library(shiny) 17 | ui <- fluidPage( 18 | tagsTextInput("fruits", "Fruits", "apple, banana"), 19 | textOutput("out") 20 | ) 21 | 22 | server <- function(input, output){ 23 | output$out <- renderPrint( strsplit( input$fruits, ",")[[1]] ) 24 | } 25 | 26 | shinyApp( ui, server ) 27 | ``` 28 | 29 |  30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ``` r 4 | library(shiny) 5 | ui <- fluidPage( 6 | tagsTextInput("fruits", "Fruits", "apple, banana"), 7 | textOutput("out") 8 | ) 9 | 10 | server <- function(input, output){ 11 | output$out <- renderPrint( strsplit( input$fruits, ",")[[1]] ) 12 | } 13 | 14 | shinyApp( ui, server ) 15 | ``` 16 | 17 |  18 | -------------------------------------------------------------------------------- /gif/fruits.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThinkR-open/tagsinput/9aa70ec34c6fa60ef317446daef4cfaf3b682d1d/gif/fruits.gif -------------------------------------------------------------------------------- /inst/js/bootstrap-tagsinput.css: -------------------------------------------------------------------------------- 1 | .bootstrap-tagsinput { 2 | background-color: #fff; 3 | border: 1px solid #ccc; 4 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 5 | display: inline-block; 6 | padding: 4px 6px; 7 | color: #555; 8 | vertical-align: middle; 9 | border-radius: 4px; 10 | max-width: 100%; 11 | line-height: 22px; 12 | cursor: text; 13 | } 14 | .bootstrap-tagsinput input { 15 | border: none; 16 | box-shadow: none; 17 | outline: none; 18 | background-color: transparent; 19 | padding: 0 6px; 20 | margin: 0; 21 | width: auto; 22 | max-width: inherit; 23 | } 24 | .bootstrap-tagsinput.form-control input::-moz-placeholder { 25 | color: #777; 26 | opacity: 1; 27 | } 28 | .bootstrap-tagsinput.form-control input:-ms-input-placeholder { 29 | color: #777; 30 | } 31 | .bootstrap-tagsinput.form-control input::-webkit-input-placeholder { 32 | color: #777; 33 | } 34 | .bootstrap-tagsinput input:focus { 35 | border: none; 36 | box-shadow: none; 37 | } 38 | .bootstrap-tagsinput .tag { 39 | margin-right: 2px; 40 | color: white; 41 | } 42 | .bootstrap-tagsinput .tag [data-role="remove"] { 43 | margin-left: 8px; 44 | cursor: pointer; 45 | } 46 | .bootstrap-tagsinput .tag [data-role="remove"]:after { 47 | content: "x"; 48 | padding: 0px 2px; 49 | } 50 | .bootstrap-tagsinput .tag [data-role="remove"]:hover { 51 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 52 | } 53 | .bootstrap-tagsinput .tag [data-role="remove"]:hover:active { 54 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 55 | } 56 | -------------------------------------------------------------------------------- /inst/js/bootstrap-tagsinput.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | "use strict"; 3 | 4 | var defaultOptions = { 5 | tagClass: function(item) { 6 | return 'label label-info'; 7 | }, 8 | itemValue: function(item) { 9 | return item ? item.toString() : item; 10 | }, 11 | itemText: function(item) { 12 | return this.itemValue(item); 13 | }, 14 | itemTitle: function(item) { 15 | return null; 16 | }, 17 | freeInput: true, 18 | addOnBlur: true, 19 | maxTags: undefined, 20 | maxChars: undefined, 21 | confirmKeys: [13, 44], 22 | delimiter: ',', 23 | delimiterRegex: null, 24 | cancelConfirmKeysOnEmpty: true, 25 | onTagExists: function(item, $tag) { 26 | $tag.hide().fadeIn(); 27 | }, 28 | trimValue: false, 29 | allowDuplicates: false 30 | }; 31 | 32 | /** 33 | * Constructor function 34 | */ 35 | function TagsInput(element, options) { 36 | this.itemsArray = []; 37 | 38 | this.$element = $(element); 39 | this.$element.hide(); 40 | 41 | this.isSelect = (element.tagName === 'SELECT'); 42 | this.multiple = (this.isSelect && element.hasAttribute('multiple')); 43 | this.objectItems = options && options.itemValue; 44 | this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; 45 | this.inputSize = Math.max(1, this.placeholderText.length); 46 | 47 | this.$container = $('
'); 48 | this.$input = $('').appendTo(this.$container); 49 | 50 | this.$element.before(this.$container); 51 | 52 | this.build(options); 53 | } 54 | 55 | TagsInput.prototype = { 56 | constructor: TagsInput, 57 | 58 | /** 59 | * Adds the given item as a new tag. Pass true to dontPushVal to prevent 60 | * updating the elements val() 61 | */ 62 | add: function(item, dontPushVal, options) { 63 | var self = this; 64 | 65 | if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) 66 | return; 67 | 68 | // Ignore falsey values, except false 69 | if (item !== false && !item) 70 | return; 71 | 72 | // Trim value 73 | if (typeof item === "string" && self.options.trimValue) { 74 | item = $.trim(item); 75 | } 76 | 77 | // Throw an error when trying to add an object while the itemValue option was not set 78 | if (typeof item === "object" && !self.objectItems) 79 | throw("Can't add objects when itemValue option is not set"); 80 | 81 | // Ignore strings only containg whitespace 82 | if (item.toString().match(/^\s*$/)) 83 | return; 84 | 85 | // If SELECT but not multiple, remove current tag 86 | if (self.isSelect && !self.multiple && self.itemsArray.length > 0) 87 | self.remove(self.itemsArray[0]); 88 | 89 | if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { 90 | var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter; 91 | var items = item.split(delimiter); 92 | if (items.length > 1) { 93 | for (var i = 0; i < items.length; i++) { 94 | this.add(items[i], true); 95 | } 96 | 97 | if (!dontPushVal) 98 | self.pushVal(); 99 | return; 100 | } 101 | } 102 | 103 | var itemValue = self.options.itemValue(item), 104 | itemText = self.options.itemText(item), 105 | tagClass = self.options.tagClass(item), 106 | itemTitle = self.options.itemTitle(item); 107 | 108 | // Ignore items allready added 109 | var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; 110 | if (existing && !self.options.allowDuplicates) { 111 | // Invoke onTagExists 112 | if (self.options.onTagExists) { 113 | var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); 114 | self.options.onTagExists(item, $existingTag); 115 | } 116 | return; 117 | } 118 | 119 | // if length greater than limit 120 | if (self.items().toString().length + item.length + 1 > self.options.maxInputLength) 121 | return; 122 | 123 | // raise beforeItemAdd arg 124 | var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options}); 125 | self.$element.trigger(beforeItemAddEvent); 126 | if (beforeItemAddEvent.cancel) 127 | return; 128 | 129 | // register item in internal array and map 130 | self.itemsArray.push(item); 131 | 132 | // add a tag element 133 | 134 | var $tag = $('' + htmlEncode(itemText) + ''); 135 | $tag.data('item', item); 136 | self.findInputWrapper().before($tag); 137 | $tag.after(' '); 138 | 139 | // add if item represents a value not present in one of the 's options 140 | if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) { 141 | var $option = $(''); 142 | $option.data('item', item); 143 | $option.attr('value', itemValue); 144 | self.$element.append($option); 145 | } 146 | 147 | if (!dontPushVal) 148 | self.pushVal(); 149 | 150 | // Add class when reached maxTags 151 | if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength) 152 | self.$container.addClass('bootstrap-tagsinput-max'); 153 | 154 | self.$element.trigger($.Event('itemAdded', { item: item, options: options })); 155 | }, 156 | 157 | /** 158 | * Removes the given item. Pass true to dontPushVal to prevent updating the 159 | * elements val() 160 | */ 161 | remove: function(item, dontPushVal, options) { 162 | var self = this; 163 | 164 | if (self.objectItems) { 165 | if (typeof item === "object") 166 | item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } ); 167 | else 168 | item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } ); 169 | 170 | item = item[item.length-1]; 171 | } 172 | 173 | if (item) { 174 | var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options }); 175 | self.$element.trigger(beforeItemRemoveEvent); 176 | if (beforeItemRemoveEvent.cancel) 177 | return; 178 | 179 | $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove(); 180 | $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove(); 181 | if($.inArray(item, self.itemsArray) !== -1) 182 | self.itemsArray.splice($.inArray(item, self.itemsArray), 1); 183 | } 184 | 185 | if (!dontPushVal) 186 | self.pushVal(); 187 | 188 | // Remove class when reached maxTags 189 | if (self.options.maxTags > self.itemsArray.length) 190 | self.$container.removeClass('bootstrap-tagsinput-max'); 191 | 192 | self.$element.trigger($.Event('itemRemoved', { item: item, options: options })); 193 | }, 194 | 195 | /** 196 | * Removes all items 197 | */ 198 | removeAll: function() { 199 | var self = this; 200 | 201 | $('.tag', self.$container).remove(); 202 | $('option', self.$element).remove(); 203 | 204 | while(self.itemsArray.length > 0) 205 | self.itemsArray.pop(); 206 | 207 | self.pushVal(); 208 | }, 209 | 210 | /** 211 | * Refreshes the tags so they match the text/value of their corresponding 212 | * item. 213 | */ 214 | refresh: function() { 215 | var self = this; 216 | $('.tag', self.$container).each(function() { 217 | var $tag = $(this), 218 | item = $tag.data('item'), 219 | itemValue = self.options.itemValue(item), 220 | itemText = self.options.itemText(item), 221 | tagClass = self.options.tagClass(item); 222 | 223 | // Update tag's class and inner text 224 | $tag.attr('class', null); 225 | $tag.addClass('tag ' + htmlEncode(tagClass)); 226 | $tag.contents().filter(function() { 227 | return this.nodeType == 3; 228 | })[0].nodeValue = htmlEncode(itemText); 229 | 230 | if (self.isSelect) { 231 | var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; }); 232 | option.attr('value', itemValue); 233 | } 234 | }); 235 | }, 236 | 237 | /** 238 | * Returns the items added as tags 239 | */ 240 | items: function() { 241 | return this.itemsArray; 242 | }, 243 | 244 | /** 245 | * Assembly value by retrieving the value of each item, and set it on the 246 | * element. 247 | */ 248 | pushVal: function() { 249 | var self = this, 250 | val = $.map(self.items(), function(item) { 251 | return self.options.itemValue(item).toString(); 252 | }); 253 | 254 | self.$element.val(val, true).trigger('change'); 255 | }, 256 | 257 | /** 258 | * Initializes the tags input behaviour on the element 259 | */ 260 | build: function(options) { 261 | var self = this; 262 | 263 | self.options = $.extend({}, defaultOptions, options); 264 | // When itemValue is set, freeInput should always be false 265 | if (self.objectItems) 266 | self.options.freeInput = false; 267 | 268 | makeOptionItemFunction(self.options, 'itemValue'); 269 | makeOptionItemFunction(self.options, 'itemText'); 270 | makeOptionFunction(self.options, 'tagClass'); 271 | 272 | // Typeahead Bootstrap version 2.3.2 273 | if (self.options.typeahead) { 274 | var typeahead = self.options.typeahead || {}; 275 | 276 | makeOptionFunction(typeahead, 'source'); 277 | 278 | self.$input.typeahead($.extend({}, typeahead, { 279 | source: function (query, process) { 280 | function processItems(items) { 281 | var texts = []; 282 | 283 | for (var i = 0; i < items.length; i++) { 284 | var text = self.options.itemText(items[i]); 285 | map[text] = items[i]; 286 | texts.push(text); 287 | } 288 | process(texts); 289 | } 290 | 291 | this.map = {}; 292 | var map = this.map, 293 | data = typeahead.source(query); 294 | 295 | if ($.isFunction(data.success)) { 296 | // support for Angular callbacks 297 | data.success(processItems); 298 | } else if ($.isFunction(data.then)) { 299 | // support for Angular promises 300 | data.then(processItems); 301 | } else { 302 | // support for functions and jquery promises 303 | $.when(data) 304 | .then(processItems); 305 | } 306 | }, 307 | updater: function (text) { 308 | self.add(this.map[text]); 309 | return this.map[text]; 310 | }, 311 | matcher: function (text) { 312 | return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1); 313 | }, 314 | sorter: function (texts) { 315 | return texts.sort(); 316 | }, 317 | highlighter: function (text) { 318 | var regex = new RegExp( '(' + this.query + ')', 'gi' ); 319 | return text.replace( regex, "$1" ); 320 | } 321 | })); 322 | } 323 | 324 | // typeahead.js 325 | if (self.options.typeaheadjs) { 326 | var typeaheadConfig = null; 327 | var typeaheadDatasets = {}; 328 | 329 | // Determine if main configurations were passed or simply a dataset 330 | var typeaheadjs = self.options.typeaheadjs; 331 | if ($.isArray(typeaheadjs)) { 332 | typeaheadConfig = typeaheadjs[0]; 333 | typeaheadDatasets = typeaheadjs[1]; 334 | } else { 335 | typeaheadDatasets = typeaheadjs; 336 | } 337 | 338 | self.$input.typeahead(typeaheadConfig, typeaheadDatasets).on('typeahead:selected', $.proxy(function (obj, datum) { 339 | if (typeaheadDatasets.valueKey) 340 | self.add(datum[typeaheadDatasets.valueKey]); 341 | else 342 | self.add(datum); 343 | self.$input.typeahead('val', ''); 344 | }, self)); 345 | } 346 | 347 | self.$container.on('click', $.proxy(function(event) { 348 | if (! self.$element.attr('disabled')) { 349 | self.$input.removeAttr('disabled'); 350 | } 351 | self.$input.focus(); 352 | }, self)); 353 | 354 | if (self.options.addOnBlur && self.options.freeInput) { 355 | self.$input.on('focusout', $.proxy(function(event) { 356 | // HACK: only process on focusout when no typeahead opened, to 357 | // avoid adding the typeahead text as tag 358 | if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) { 359 | self.add(self.$input.val()); 360 | self.$input.val(''); 361 | } 362 | }, self)); 363 | } 364 | 365 | 366 | self.$container.on('keydown', 'input', $.proxy(function(event) { 367 | var $input = $(event.target), 368 | $inputWrapper = self.findInputWrapper(); 369 | 370 | if (self.$element.attr('disabled')) { 371 | self.$input.attr('disabled', 'disabled'); 372 | return; 373 | } 374 | 375 | switch (event.which) { 376 | // BACKSPACE 377 | case 8: 378 | if (doGetCaretPosition($input[0]) === 0) { 379 | var prev = $inputWrapper.prev(); 380 | if (prev.length) { 381 | self.remove(prev.data('item')); 382 | } 383 | } 384 | break; 385 | 386 | // DELETE 387 | case 46: 388 | if (doGetCaretPosition($input[0]) === 0) { 389 | var next = $inputWrapper.next(); 390 | if (next.length) { 391 | self.remove(next.data('item')); 392 | } 393 | } 394 | break; 395 | 396 | // LEFT ARROW 397 | case 37: 398 | // Try to move the input before the previous tag 399 | var $prevTag = $inputWrapper.prev(); 400 | if ($input.val().length === 0 && $prevTag[0]) { 401 | $prevTag.before($inputWrapper); 402 | $input.focus(); 403 | } 404 | break; 405 | // RIGHT ARROW 406 | case 39: 407 | // Try to move the input after the next tag 408 | var $nextTag = $inputWrapper.next(); 409 | if ($input.val().length === 0 && $nextTag[0]) { 410 | $nextTag.after($inputWrapper); 411 | $input.focus(); 412 | } 413 | break; 414 | default: 415 | // ignore 416 | } 417 | 418 | // Reset internal input's size 419 | var textLength = $input.val().length, 420 | wordSpace = Math.ceil(textLength / 5), 421 | size = textLength + wordSpace + 1; 422 | $input.attr('size', Math.max(this.inputSize, $input.val().length)); 423 | }, self)); 424 | 425 | self.$container.on('keypress', 'input', $.proxy(function(event) { 426 | var $input = $(event.target); 427 | 428 | if (self.$element.attr('disabled')) { 429 | self.$input.attr('disabled', 'disabled'); 430 | return; 431 | } 432 | 433 | var text = $input.val(), 434 | maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars; 435 | if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) { 436 | // Only attempt to add a tag if there is data in the field 437 | if (text.length !== 0) { 438 | self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text); 439 | $input.val(''); 440 | } 441 | 442 | // If the field is empty, let the event triggered fire as usual 443 | if (self.options.cancelConfirmKeysOnEmpty === false) { 444 | event.preventDefault(); 445 | } 446 | } 447 | 448 | // Reset internal input's size 449 | var textLength = $input.val().length, 450 | wordSpace = Math.ceil(textLength / 5), 451 | size = textLength + wordSpace + 1; 452 | $input.attr('size', Math.max(this.inputSize, $input.val().length)); 453 | }, self)); 454 | 455 | // Remove icon clicked 456 | self.$container.on('click', '[data-role=remove]', $.proxy(function(event) { 457 | if (self.$element.attr('disabled')) { 458 | return; 459 | } 460 | self.remove($(event.target).closest('.tag').data('item')); 461 | }, self)); 462 | 463 | // Only add existing value as tags when using strings as tags 464 | if (self.options.itemValue === defaultOptions.itemValue) { 465 | if (self.$element[0].tagName === 'INPUT') { 466 | self.add(self.$element.val()); 467 | } else { 468 | $('option', self.$element).each(function() { 469 | self.add($(this).attr('value'), true); 470 | }); 471 | } 472 | } 473 | }, 474 | 475 | /** 476 | * Removes all tagsinput behaviour and unregsiter all event handlers 477 | */ 478 | destroy: function() { 479 | var self = this; 480 | 481 | // Unbind events 482 | self.$container.off('keypress', 'input'); 483 | self.$container.off('click', '[role=remove]'); 484 | 485 | self.$container.remove(); 486 | self.$element.removeData('tagsinput'); 487 | self.$element.show(); 488 | }, 489 | 490 | /** 491 | * Sets focus on the tagsinput 492 | */ 493 | focus: function() { 494 | this.$input.focus(); 495 | }, 496 | 497 | /** 498 | * Returns the internal input element 499 | */ 500 | input: function() { 501 | return this.$input; 502 | }, 503 | 504 | /** 505 | * Returns the element which is wrapped around the internal input. This 506 | * is normally the $container, but typeahead.js moves the $input element. 507 | */ 508 | findInputWrapper: function() { 509 | var elt = this.$input[0], 510 | container = this.$container[0]; 511 | while(elt && elt.parentNode !== container) 512 | elt = elt.parentNode; 513 | 514 | return $(elt); 515 | } 516 | }; 517 | 518 | /** 519 | * Register JQuery plugin 520 | */ 521 | $.fn.tagsinput = function(arg1, arg2, arg3) { 522 | var results = []; 523 | 524 | this.each(function() { 525 | var tagsinput = $(this).data('tagsinput'); 526 | // Initialize a new tags input 527 | if (!tagsinput) { 528 | tagsinput = new TagsInput(this, arg1); 529 | $(this).data('tagsinput', tagsinput); 530 | results.push(tagsinput); 531 | 532 | if (this.tagName === 'SELECT') { 533 | $('option', $(this)).attr('selected', 'selected'); 534 | } 535 | 536 | // Init tags from $(this).val() 537 | $(this).val($(this).val()); 538 | } else if (!arg1 && !arg2) { 539 | // tagsinput already exists 540 | // no function, trying to init 541 | results.push(tagsinput); 542 | } else if(tagsinput[arg1] !== undefined) { 543 | // Invoke function on existing tags input 544 | if(tagsinput[arg1].length === 3 && arg3 !== undefined){ 545 | var retVal = tagsinput[arg1](arg2, null, arg3); 546 | }else{ 547 | var retVal = tagsinput[arg1](arg2); 548 | } 549 | if (retVal !== undefined) 550 | results.push(retVal); 551 | } 552 | }); 553 | 554 | if ( typeof arg1 == 'string') { 555 | // Return the results from the invoked function calls 556 | return results.length > 1 ? results : results[0]; 557 | } else { 558 | return results; 559 | } 560 | }; 561 | 562 | $.fn.tagsinput.Constructor = TagsInput; 563 | 564 | /** 565 | * Most options support both a string or number as well as a function as 566 | * option value. This function makes sure that the option with the given 567 | * key in the given options is wrapped in a function 568 | */ 569 | function makeOptionItemFunction(options, key) { 570 | if (typeof options[key] !== 'function') { 571 | var propertyName = options[key]; 572 | options[key] = function(item) { return item[propertyName]; }; 573 | } 574 | } 575 | function makeOptionFunction(options, key) { 576 | if (typeof options[key] !== 'function') { 577 | var value = options[key]; 578 | options[key] = function() { return value; }; 579 | } 580 | } 581 | /** 582 | * HtmlEncodes the given value 583 | */ 584 | var htmlEncodeContainer = $(''); 585 | function htmlEncode(value) { 586 | if (value) { 587 | return htmlEncodeContainer.text(value).html(); 588 | } else { 589 | return ''; 590 | } 591 | } 592 | 593 | /** 594 | * Returns the position of the caret in the given input field 595 | * http://flightschool.acylt.com/devnotes/caret-position-woes/ 596 | */ 597 | function doGetCaretPosition(oField) { 598 | var iCaretPos = 0; 599 | if (document.selection) { 600 | oField.focus (); 601 | var oSel = document.selection.createRange(); 602 | oSel.moveStart ('character', -oField.value.length); 603 | iCaretPos = oSel.text.length; 604 | } else if (oField.selectionStart || oField.selectionStart == '0') { 605 | iCaretPos = oField.selectionStart; 606 | } 607 | return (iCaretPos); 608 | } 609 | 610 | /** 611 | * Returns boolean indicates whether user has pressed an expected key combination. 612 | * @param object keyPressEvent: JavaScript event object, refer 613 | * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html 614 | * @param object lookupList: expected key combinations, as in: 615 | * [13, {which: 188, shiftKey: true}] 616 | */ 617 | function keyCombinationInList(keyPressEvent, lookupList) { 618 | var found = false; 619 | $.each(lookupList, function (index, keyCombination) { 620 | if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) { 621 | found = true; 622 | return false; 623 | } 624 | 625 | if (keyPressEvent.which === keyCombination.which) { 626 | var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey, 627 | shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey, 628 | ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey; 629 | if (alt && shift && ctrl) { 630 | found = true; 631 | return false; 632 | } 633 | } 634 | }); 635 | 636 | return found; 637 | } 638 | 639 | /** 640 | * Initialize tagsinput behaviour on inputs and selects which have 641 | * data-role=tagsinput 642 | */ 643 | $(function() { 644 | $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput(); 645 | }); 646 | })(window.jQuery); 647 | -------------------------------------------------------------------------------- /man/tagsTextInput.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/tagsTextInput.R 3 | \name{tagsTextInput} 4 | \alias{tagsTextInput} 5 | \title{text input specific to tags} 6 | \usage{ 7 | tagsTextInput(...) 8 | } 9 | \arguments{ 10 | \item{...}{see \code{\link[shiny]{textInput}}} 11 | } 12 | \description{ 13 | text input specific to tags 14 | } 15 | \examples{ 16 | \dontrun{ 17 | library(shiny) 18 | ui <- fluidPage( 19 | tagsTextInput("fruits", "Fruits", "apple, banana"), 20 | textOutput("out") 21 | ) 22 | 23 | server <- function(input, output){ 24 | output$out <- renderPrint( strsplit( input$fruits, ",")[[1]] ) 25 | } 26 | 27 | shinyApp( ui, server ) 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tagsinput.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | 18 | BuildType: Package 19 | PackageUseDevtools: Yes 20 | PackageInstallArgs: --no-multiarch --with-keep.source 21 | PackageRoxygenize: rd,collate,namespace,vignette 22 | --------------------------------------------------------------------------------