Copy and paste text in selectizeInput in a Shiny application - javascript

I have a selectizeInput UI object in my Shiny app and I would like to be able to copy and paste a comma-delimited list of inputs (i.e. copy from and paste to the selectizeInput).
At the moment I can copy the comma-delimited list of inputs (i.e. A, B, C, D) from elsewhere and then paste it in my selectizeInput. Paste only works using "Ctrl + V", not "right-click + paste", but this is fine.
I would like to be able to also copy my inputs from the selectizeInput object so I can paste them elsewhere.
See code below (the first choice is an empty string, "", as I do not want anything to be selected at the beginning):
selectizeInput(
inputId = "genes_list",
label = "Genes",
width = "100%",
multiple = TRUE,
choices = c("", genes),
selected = "",
options = list(
delimiter = ',',
create = I("function(input, callback){
return {
value: input,
text: input };
}")))
I can select all inputs using "Ctrl + A" or specific inputs using "Ctrl + mouse-click" (I know inputs have been selected as they change color when selected) but then "Ctrl + C" or "Ctrl + X" do not work. Also, right-clicking on the selected inputs does not provide a "Copy" option.
Ideally, I would like to use "Ctrl + A" or "Ctrl + mouse-click" to select my inputs and then use "Ctrl + C" to copy them.
Thanks

This is a bit of a long-winded solution, but it works. It injects a javascript behaviour into your selectizeInput of copying into clipboard/pastebin when a person uses copy-paste shortcuts.
There are much cleaner ways to do it, but they require more advanced concepts, like separate .js files. So here is the essier, but messier way.
Below is the code, so you see roughly what it does (all the console.log() bits can be removed and are there for you to see all steps and how they happen. To see the 'console' onen dev tools in your browser, and there is a console panel there (it's sort of like a 'kitchen-door/gossip wall' of your app).
Here's the javascript that will do it, below is the explanation where to add it. In short it will:
once the page is loaded
add a new behaviour whenever someone tries to copy
when someone tries to copy, check if the field they copy from is a selectize-input
if it is, grab text in it, separate it by comas, and put thsat in the clipboard/pastebin
Javascript Code:
console.log("page will load now");
document.addEventListener("DOMContentLoaded", function(){
console.log("page loaded");
document.addEventListener("copy", (event) => {
console.log("coppying from item:", event.target);
const anchorNode = document.getSelection().anchorNode
if (anchorNode instanceof HTMLElement && anchorNode.classList.contains("selectize-input")) {
const items = Array.from(anchorNode.getElementsByClassName("item active"))
const selectedItemsAsString = items.map(i => i.innerText).join(", ")
console.log("coppied content:", selectedItemsAsString);
event.clipboardData.setData("text/plain", selectedItemsAsString)
event.preventDefault()
}
})
});
Explanation where to put it, at the end of your ui <- fluidPage( ..... )
ui <- fluidPage( some_components_of_yours,
selectizeInput(
"codeInput",
label = "Codes (if pasting, coma separated)",
choices = c("", genes),
multiple = T,
options = list(delimiter = ",", create = T),
),
some_components_of_yours,
tags$script(HTML(
'HERE IN THESE SINGLE QUOTES PUT THE JAVASCRIPT CODE FROM ABOVE'
)))
So it will look a bit like this:
ui <- fluidPage(
selectizeInput(
"codeInput",
label = "Codes (if pasting, coma separated)",
choices = c("", genes),
multiple = T,
options = list(delimiter = ",", create = T),
),
tags$script(HTML(
' console.log("page will load now");
document.addEventListener("DOMContentLoaded", function(){
console.log("page loaded");
document.addEventListener("copy", (event) => {
console.log("coppying from item:", event.target);
const anchorNode = document.getSelection().anchorNode
if (anchorNode instanceof HTMLElement && anchorNode.classList.contains("selectize-input")) {
const items = Array.from(anchorNode.getElementsByClassName("item active"))
const selectedItemsAsString = items.map(i => i.innerText).join(", ")
console.log("coppied content:", selectedItemsAsString);
event.clipboardData.setData("text/plain", selectedItemsAsString)
event.preventDefault()
}
})
});'
)))
My solution is based on these answers of other people:
https://stackoverflow.com/a/59773533/498201
https://github.com/selectize/selectize.js/issues/1177#issuecomment-480633418

Related

Sending Javascript in Shiny

I am trying to change an inputs on Shiny using JS with an input from the server (so the value can be conditional).
In this example I've fixed the value to 'NJ' but at the end id like this value to be dynamic. The end result I am trying to achieve is to get the dropdown menu to change to a value (here 'NJ') when the button is clicked. Working example below.
My JS knowledge is pretty light and most of it has come from reading the R documentation, such as the doc below.
https://shiny.rstudio.com/articles/communicating-with-js.html
library("shiny")
ui <- fluidPage(
tags$script("Shiny.addCustomMessageHandler('change_sel', function(x){
Shiny.setInputValue('state', x,{priority: 'event'});
});")
,
column(6,
selectInput("state", "Choose a state:",
c("NY", "NJ", "CT")
),
column(6,
actionButton("but1", "Change to NJ"
)
)))
server <- function(input, output, session) {
observeEvent(input$but1, {
session$sendCustomMessage("change_sel", "NJ")
})
observe(print(input$state))
}
shinyApp(ui, server)
If you are using shiny input components and would like to update the value of an input element by some other means (e.g., button click), then use one of the update- functions (available in the shiny package). However, it is unlikely that these update functions will work (or are even available) for non-Shiny widgets.
One alternative is to create a custom input binding where data is sent between Shiny and the client. This approach is best for custom input components or for creating wrappers around existing component libraries (non-R/Shiny packages). (For more information, checkout the following RStudio guides Building Custom Input Objects and How to create custom input bindings. I won't go into detail here about input bindings as it would make for a long answer).
Messages can be sent from Shiny to the client using session$sendInputMessage(). In the input binding (written using jQuery; vanilla JS won't work), use the receiveInputMessage method to retrieve and respond to the data. Using the example provided in your question, I provided a demo on how to update a custom select input widget in response to a button click.
Let me know if you have any questions!
library(shiny)
ui <- tagList(
tags$main(
tags$h1("Receive Message Input"),
tags$label(
`for` = "state",
"Select a State"
),
tags$select(
id = "state",
class = "select__input",
tags$option(value = "NONE", "--- Select ---"),
tags$option(value = "NY", "New York"),
tags$option(value = "NJ", "New Jersey"),
tags$option(value = "CT", "Connecticut")
),
tags$button(
id = "state_ny",
class = "shiny-bound-input action-button",
"Select New York"
),
tags$button(
id = "state_nj",
class = "shiny-bound-input action-button",
"Select New Jersey"
),
tags$button(
id = "state_reset",
class = "shiny-bound-input action-button",
"Reset"
)
),
tags$script(
type = "text/javascript",
HTML(
"const myCustomBinding = new Shiny.InputBinding();",
"$.extend(myCustomBinding, {
find = function(scope) {
return $(scope).find('.select__input');
},
getValue = function(el) {
return $(scope).value;
},
subscribe = function(el, callback) {
$(el).on('change', function(e) {
callback();
});
},
receiveInputMessage = function(el, message) {
if (message.type === 'update') {
$(el).value = data.value
}
},
unsubscribe = function(el) {
$(el).off('myCustomBinding');
}
});",
"Shiny.inputBindings.register(myCustomBinding);"
)
)
)
server <- function(input, output, session) {
# server-side function for sending data for use in `receiveInputMessage`
setSelectValue <- function(inputId, value) {
session$sendInputMessage(
inputId = inputId,
message = list(
type = "update",
value = value
)
)
}
# onClick Events
observeEvent(input$state_ny, setSelectValue("state", "NY"))
observeEvent(input$state_nj, setSelectValue("state", "NJ"))
observeEvent(input$state_reset, setSelectValue("state", "NONE"))
observe(print(input$state))
}
shinyApp(ui, server)

How to remove buttons in tinymce editor for the non-default wordpress editor

I can see numerous examples on removing buttons in the tinymce editor but I want to do this for a custom editor I am adding from Javascript.
function myplugin_tinymce_buttons( $buttons ) {
//Remove the text color selector
$remove = 'forecolor';
//Find the array key and then unset
if ( ( $key = array_search( $remove, $buttons ) ) !== false )
unset( $buttons[$key] );
return $buttons;
}
There is no mention of editor ID here. How do I do this only for a custom editor? I dont want to change anything in the main editor shown in Wordpress post page.
The best and cleanest way is definitely to change your TinyMCE config before initialization.
Otherwise you can refer to my answer on another question where I set the editor in ReadOnly mode then enable just few buttons.
I didn't test this code but your function should be something like this:
function removeButton(editorId, pluginName, commandName) {
var htmlEditorDiv = document.getElementById(editorId).previousSibling;
var editor = tinymce.get(editorId);
var buttonDiv = htmlEditorDiv.querySelectorAll('.mce-i-' + pluginName.toLowerCase())[0].parentElement.parentElement;
buttonDiv.style.display = "none";
buttonDiv.firstChild.onclick = function () {
//Even if the button is invisible, it's better
//removing the command associated to the button click just in case
};
}
For the list of commands, refer to this page

Hiding field of custom-multi-field using javascript in listener

I have customized form of multi-field in a component having two variations.
In one variation of my component I want to hide a field (title) which is inside custom-multi-field . I am using the following JavaScript code in listener.
This code is not working. Where am I wrong?
function() {
var dialog = this.findParentByType('dialog');
var contenttype = dialog.getField("./type").getValue();
var teaserlinks = dialog.getField("./teaserlinks");
var title = dialog.getField("./teaserlinks").getField("./title");
alert(title);
if(contenttype == 'variation-1'){
teaserlinks.show();
title.hide();
}
else if(contenttype == 'variation-2'){
teaserlinks.show();
}
}
Try using the hidden property of node. Initially set the hidden property to true and in javascript file change the hidden property to false (or as per your requirement).
Few imp points first before answer:
you have to write listener in your widget file only.
below is the sample code where in I have 2 fields. 1st field is mytext field and another field is myselection. On changing the value in myselection field I am toggling visibility of my text field.
Below is snippet:
this.mytext = new CQ.form.textField({...})
this.myselection = new CQ.form.Selection({
fieldLabel:"my selection",
type:"select",
width : "325",
allowBlank:false,
defaultType:"String[]",
fieldDescription : "Select value from dropdown",
options: "/a/b/c.json",
listeners : {
selectionchanged : function(){
var mytext = this.findParentByType('mywidget').mytext;
mytext.hide();
}
}
});
I hope this will be helpful.
I have no knowledge about aem and Adobe CQ5 but I can give you some hints how to debug your script.
First of all don't use alert for debugging! (BTW what does alert(title); show?)
I would recommend to open the browser console (e.g. Press <F12> on Firefox and switch to the tab "Console").
Herein the browser displays all exceptions and error messages. Additionally you can output some text with console.log("...");` from your script.
Here is my edit of your program. Perhaps the output can help you.
function()
{
var dialog = this.findParentByType('dialog');
var contenttype = dialog.getField("./type").getValue();
var teaserlinks = dialog.getField("./teaserlinks");
var title = dialog.getField("./teaserlinks").getField("./title");
console.dir(title);
console.log(contenttype);
teaserlinks.show();
if(contenttype == 'variation-1')
{
title.hide();
}
else if(contenttype == 'variation-2')
{
title.show();
}
}
And, console.dir(<object>); shows you the object structure to one level deep.

How to match multiple substrings in jQuery combobox autocomplete

I found more than a couple examples of this with a plain jquery autocomplete but not in a way that will work with the autocomplete included in the combobox code from the demo because the structure of the code is structured so differently.
I want to match every item that has all of the search tokens anywhere in any word. I don't need to match the start of any word, any part of it is fine. I don't care if the search strings are highlighted in the autocomplete list if that makes things too complicated.
Desired search/result combos: (please excuse the spacing)
"fi th" "fi rst second th ird"
"rs on" "fi rs t sec on d third"
"ec rd" "first s ec ond thi rd"
but not limited to any max/min length or number of tokens.
EDIT
I figured part of it out using the code structure from the other autocorrect I had working.
source: function( requestObj, responseFunc ) {
var matchArry = $("select > option").map(function(){return this.innerHTML;}).get();
var srchTerms = $.trim(requestObj.term).split(/\s+/);
// For each search term, remove non-matches
$.each (srchTerms, function (J, term) {
var regX = new RegExp (term, "i");
matchArry = $.map (matchArry, function (item) {
if( regX.test(item) ){
return{
label: item,
value: item,
option: HTMLOptionElement
} ? item :null;
}
} );
});
// Return the match results
responseFunc (matchArry);
},
and
select: function( event, ui ) {
ui.item.option.selected = true;
self._trigger( "selected", event, {
item: ui.item.option
});
$("destination").val(ui.item.value); // I added this line
},
but I can't get both multiple words AND being able to click to select working at the same time.
If I remove the } ? item :null; on the return in the map function I can click to select an item. If I leave it I can type multiple words, but I can't click any of the items...
Is that the problem or the option: this? I've tried replacing it with HTMLOptionElement and null and I'm stuck.
I am able to set the value of another field with ui.item.value within the select label but that doesn't put the value in the search box or close the dropdown menu.
On a whim I added ui.item.option = ui.item.value; to the select label and everything works as expected. the option: value in source doesn't seem to matter now.
*I make no claims that any of this is good coding practice
updated fiddle: http://jsfiddle.net/eY3hM/

CodeMirror2: How to format a pasted content?

Is it possible to format the inserted content after an event like "onPaste" in CodeMirror2? - I want to indent the content from the clipboard after pasting. I already know with JavaScript it's not possible to get access to the clipboard.
So I think there is also no possiblity to create a context menu with cut/copy/paste functions? - Could I create my own JS-clipboard or is there an existing solution?
Thanks!
leX
Took me a little while to work this out, so in case it helps anyone, here's how I'm intercepting pastes and replacing each tab with 2 spaces:
editor.on("beforeChange", (cm, change) => {
if(change.origin === "paste") {
const newText = change.text.map(line => line.replace(/\t/g, " "));
change.update(null, null, newText);
}
});
A CodeMirror has native "inputRead" event so you can do this:
editor.on('inputRead', function(cm, event) {
/* event -> object{
origin: string, can be '+input', '+move' or 'paste'
doc for origins >> http://codemirror.net/doc/manual.html#selection_origin
from: object {line, ch},
to: object {line, ch},
removed: array of removed strings
text: array of pasted strings
} */
if (event.origin == 'paste') {
console.log(event.text);
var text = event.text[0]; // pasted string
var new_text = '['+text+']'; // any operations here
cm.refresh();
// my first idea was
// note: for multiline strings may need more complex calculations
cm.replaceRange(new_text, event.from, {line: event.from.line, ch: event.from.ch + text.length});
// first solution did'nt work (before i guess to call refresh) so i tried that way, works too
/* cm.execCommand('undo');
cm.setCursor(event.from);
cm.replaceSelection(new_text); */
}
});
And there are also other events "cut", "copy" and "paste" (http://codemirror.net/doc/manual.html#events) so this will work:
editor.on('paste', function(cm, event) { ... } );
Andavtage is you can cancel event by calling event.preventDefault(); but retrieving pasted text is a problem.
Currently i am working on similar thing - hook copy/paste events and make some replacements. I found a solution to programmatically copy text to clipboard. This is clipboard.js
discussed here Does codemirror provide Cut, Copy and Paste API?. The great thing is you can trigger click event programmatically (that was a problem when i used ZeroClipboard with cross-browser flash shim) and it will work!
new Clipboard('.btn-copy', {
text: function(trigger) {
var text = editor.getValue(); // or editor.getSelection();
return text.replace(/\s+/g,' ');
}
});
editor.on('copy', function(cm, event) {
$('.btn-copy').click();
event.preventDefault();
});
The only solution I found was intercepting the even using inputRead and then manually changing the pasted contents. I made a generic funcion you could reuse:
class CodeMirrorExt {
// This function returns a callback to be used with codemirror's inputRead event.
// The intent is to intercept a pasted text, transform the pasted contents (each line) with the function
// you provide, and then have that transformed content be what ends up in the editor.
//
// Usage example that cuts each pasted line to 10 chars:
//
// this.codemirror.on("inputRead", CodeMirrorExt.replaceTextOnPasteFunc((line) => {
// return line.slice(0, 10);
// }));
static replaceTextOnPasteFunc(transformFunc) {
return ((doc, event) => {
if (event.origin !== "paste") {
return;
}
const firstText = event.text[0];
const lineNum = event.from.line;
const chNum = event.from.ch;
const newTexts = event.text.map(transformFunc);
newTexts.forEach((text, i) => {
if (i == 0) {
doc.replaceRange(text, {line: lineNum, ch: chNum}, {line: lineNum, ch: chNum + firstText.length});
}
else {
doc.replaceRange(text, {line: lineNum + i, ch: 0}, {line: lineNum + i, ch: event.text[i].length});
}
})
});
}
}
export default CodeMirrorExt;
Using js-beautify you can beautify the editors value during a paste event on the .CodeMirror like so:
$(document).on('paste', '.CodeMirror', function(e) {
var content = $(this).closest('.content');
var editor = content[0].editor;
// beautify the code
editor.setValue( js_beautify( editor.getValue(), { indent_size: 2 } ) );
});
Be aware that when inserting text into the editor, it listens to the first indentation, so if your first line is indented, all other lines will be indented accordingly.
ex:
function something() { // if the start is indented like so,
var blah = something; // next lines will follow
}

Categories

Resources