1

My goal is to add a search box on top of the custom input control in shiny. I would like when a user searches Hampshire for example, the selection to pick New Hampshire which is not currently possible as it searches just by the first letter.

server.R

shinyServer(function(input, output, session) {

output$main <- renderUI({
source("chooser.R")
chooserInput("mychooser","Available frobs","Selected frobs",
row.names(USArrests),c(),size=20,multiple=TRUE)})
})

ui.R

source("chooser.R")

shinyUI(fluidPage(
uiOutput("main")
))

chooser.R

chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
size = 5, multiple = FALSE) {

leftChoices <- lapply(leftChoices, tags$option)
rightChoices <- lapply(rightChoices, tags$option)

if (multiple)
  multiple <- "multiple"
else
multiple <- NULL

tagList(
singleton(tags$head(
  tags$script(src="chooser-binding.js"),
  tags$style(type="text/css",
    HTML(".chooser-container { display: inline-block; }")
  )
)),
div(id=inputId, class="chooser",
  div(class="chooser-container chooser-left-container",
    tags$select(class="left", size=size, multiple=multiple, leftChoices)
  ),
  div(class="chooser-container chooser-center-container",
    icon("arrow-circle-o-right", "right-arrow fa-3x"),
    tags$br(),
    icon("arrow-circle-o-left", "left-arrow fa-3x")
    ),
    div(class="chooser-container chooser-right-container",
    tags$select(class="right", size=size, multiple=multiple, rightChoices)
  )
)
)
}

registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
if (is.null(data))
NULL
else
list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE)  

chooser-binding.js (in www folder)

(function() {

function updateChooser(chooser) {
chooser = $(chooser);
var left = chooser.find("select.left");
var right = chooser.find("select.right");
var leftArrow = chooser.find(".left-arrow");
var rightArrow = chooser.find(".right-arrow");

var canMoveTo = (left.val() || []).length > 0;
var canMoveFrom = (right.val() || []).length > 0;

leftArrow.toggleClass("muted", !canMoveFrom);
rightArrow.toggleClass("muted", !canMoveTo);
}

function move(chooser, source, dest) {
chooser = $(chooser);
var selected = chooser.find(source).children("option:selected");
var dest = chooser.find(dest);
dest.children("option:selected").each(function(i, e) {e.selected = false;});
dest.append(selected);
updateChooser(chooser);
chooser.trigger("change");
}

$(document).on("change", ".chooser select", function() {
updateChooser($(this).parents(".chooser"));
});

$(document).on("click", ".chooser .right-arrow", function() {
move($(this).parents(".chooser"), ".left", ".right");
});

$(document).on("click", ".chooser .left-arrow", function() {
move($(this).parents(".chooser"), ".right", ".left");
});

$(document).on("dblclick", ".chooser select.left", function() {
move($(this).parents(".chooser"), ".left", ".right");
});

$(document).on("dblclick", ".chooser select.right", function() {
move($(this).parents(".chooser"), ".right", ".left");
});

var binding = new Shiny.InputBinding();

binding.find = function(scope) {
return $(scope).find(".chooser");
};

binding.initialize = function(el) {
updateChooser(el);
};

binding.getValue = function(el) {
return {
left: $.makeArray($(el).find("select.left option").map(function(i, e) { return      e.value; })),
right: $.makeArray($(el).find("select.right option").map(function(i, e) {   return e.value; }))
}
};

binding.setValue = function(el, value) {
// TODO: implement
};

binding.subscribe = function(el, callback) {
$(el).on("change.chooserBinding", function(e) {
callback();
});
};

binding.unsubscribe = function(el) {
$(el).off(".chooserBinding");
};

binding.getType = function() {
return "shinyjsexamples.chooser";
};

Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");

})();
Sam Kingston
  • 817
  • 1
  • 10
  • 19

1 Answers1

3

Cool widget (or whatever the terminology is). This question has actually been answered here so make sure to vote on the persons answer if it helps you.

Here's a super simple implementations of it (could be better):

chooser.R

chooserInput <- function(inputId, leftLabel, rightLabel, leftChoices, rightChoices,
                         size = 5, multiple = FALSE) {

  leftChoices <- lapply(leftChoices, tags$option)
  rightChoices <- lapply(rightChoices, tags$option)

  if (multiple)
    multiple <- "multiple"
  else
    multiple <- NULL

  tagList(
    singleton(tags$head(
      tags$script(src="chooser-binding.js"),
      tags$style(type="text/css",
                 HTML(".chooser-container { display: inline-block; }")
      )
    )),
    div(id=inputId, class="chooser",style="",
        div(
          div(style="min-width:100px;",
              tags$input(type="text",class="chooser-input-search",style="width:100px;")
          )
        ),
        div(style="display:table",
            div(style="min-width:100px; display:table-cell;",
                div(class="chooser-container chooser-left-container",
                    style="width:100%;",
                    tags$select(class="left", size=size, multiple=multiple, leftChoices,style="width:100%;min-width:100px")
                )
            ),
            div(style="min-width:50px; display:table-cell;vertical-align: middle;",
                div(class="chooser-container chooser-center-container",
                    style="padding:10px;",
                    icon("arrow-circle-o-right", "right-arrow fa-3x"),
                    tags$br(),
                    icon("arrow-circle-o-left", "left-arrow fa-3x")
                )
            ),
            div(style="min-width:100px; display:table-cell;",
                div(class="chooser-container chooser-right-container", style="width:100%;",
                    tags$select(class="right", size=size, multiple=multiple, rightChoices,style="width:100%;")
                )
            )
        )
    )
  )
}

registerInputHandler("shinyjsexamples.chooser", function(data, ...) {
  if (is.null(data))
    NULL
  else
    list(left=as.character(data$left), right=as.character(data$right))
}, force = TRUE) 

chooser-bindings.js

(function() {

var options = [];
jQuery.fn.filterByText = function(textbox, selectSingleMatch) {
  return this.each(function() {
    var select = this;
    options = [];
    $(select).find('option').each(function() {
      options.push({value: $(this).val(), text: $(this).text()});
    });
    $(select).data('options', options);
    $(textbox).bind('change keyup', function() {
      options = $(select).empty().scrollTop(0).data('options');
      var search = $.trim($(this).val());
      var regex = new RegExp(search,'gi');

      $.each(options, function(i) {
        var option = options[i];
        if(option.text.match(regex) !== null) {
          $(select).append(
             $('<option>').text(option.text).val(option.value)
          );
        }
      });
      if (selectSingleMatch === true && 
          $(select).children().length === 1) {
        $(select).children().get(0).selected = true;
      }
    });
  });
};

function updateChooser(chooser) {
    chooser = $(chooser);
    var left = chooser.find("select.left");
    var right = chooser.find("select.right");
    var leftArrow = chooser.find(".left-arrow");
    var rightArrow = chooser.find(".right-arrow");

    var canMoveTo = (left.val() || []).length > 0;
    var canMoveFrom = (right.val() || []).length > 0;

    leftArrow.toggleClass("muted", !canMoveFrom);
    rightArrow.toggleClass("muted", !canMoveTo);
}

function move(chooser, source, dest) {
    chooser = $(chooser);
    var selected = chooser.find(source).children("option:selected");
    var dest = chooser.find(dest);
    dest.children("option:selected").each(function(i, e) {e.selected = false;});
    dest.append(selected);
    updateChooser(chooser);
    chooser.trigger("change");
}

$(".chooser").change(function(){

});

$(document).on("change", ".chooser select", function() {
    updateChooser($(this).parents(".chooser"));
});

$(document).on("click", ".chooser .right-arrow", function() {
    move($(this).parents(".chooser"), ".left", ".right");
});

$(document).on("click", ".chooser .left-arrow", function() {
    move($(this).parents(".chooser"), ".right", ".left");
});

$(document).on("dblclick", ".chooser select.left", function() {
    move($(this).parents(".chooser"), ".left", ".right");
});

$(document).on("dblclick", ".chooser select.right", function() {
    move($(this).parents(".chooser"), ".right", ".left");
});

var binding = new Shiny.InputBinding();

binding.find = function(scope) {
    return $(scope).find(".chooser");
};

binding.initialize = function(el) {
    updateChooser(el);
    $(function() {
      $('.left').filterByText($('.chooser-input-search'), true);
    }); 
};

binding.getValue = function(el) {
return {
    left: $.makeArray($(el).find("select.left option").map(function(i, e) { return      e.value; })),
    right: $.makeArray($(el).find("select.right option").map(function(i, e) {   return e.value; }))
}
};

binding.setValue = function(el, value) {
// TODO: implement
};

binding.subscribe = function(el, callback) {
    $(el).on("change.chooserBinding", function(e) {
        callback();
    });
};

binding.unsubscribe = function(el) {
    $(el).off(".chooserBinding");
};

binding.getType = function() {
return "shinyjsexamples.chooser";
};

Shiny.inputBindings.register(binding, "shinyjsexamples.chooser");

})();

As you can see this is pretty much a shameful copy and paste.

Community
  • 1
  • 1
RmIu
  • 4,357
  • 1
  • 21
  • 24
  • Thanks so much Oskar. One last question. My version of server.R and ui.R are a bit different than what I initially posted. What you sent works with my initial post. I edited to show my version. Do you know why my version disables the search box? It is important that I keep to code on the server.R side and pass it to ui.R. – Sam Kingston Nov 26 '15 at 18:22
  • It's because my code was really bad, the element I'm binding to in the DOM doesn't exist at the time when I bind the search function so I've changed it so that the search function is bound on initialization. Also I've just hard coded some widths for the different elements but this is probably not the way to go. – RmIu Nov 26 '15 at 21:16
  • Thanks for the response. I am not sure why, but with the new code, the search box doesn't show up. – Sam Kingston Nov 27 '15 at 00:26
  • hey Oskar. I finally got it to work. I have one last question with this. I just posted it as a new question so I could give credit. If it doesn't take too much of your time, I would appreciate it if you could take a look. I can't imagine anyone other than you will attempt to solve it. – Sam Kingston Nov 27 '15 at 06:09