├── .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 | ![](gif/fruits.gif) 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 | ![](gif/fruits.gif) 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