I came across this problem when building a Shiny application in R. In the application, an action button is used to trigger a hidden download button. This would allow me to observe the action button event, react to that event, and then trigger the download process.
However, when I set the display attribute of the download button to hidden the href target of the button, which typically targets something like "session/1c47..ef8/download/download_show?w=", was missing.
Below is a smaller Shiny application which reproduces the problem.
shinyApp(
ui = fluidPage(
tags$head(
tags$style(HTML(".hide { display: none; }")),
tags$script(HTML('
Shiny.addCustomMessageHandler("trigger-button", function(message) {
document.getElementById(message.button_id).click();
});
'))
),
div(
class = "disable",
downloadButton("download_shown", "Shown"),
div(
class = "hide",
downloadButton("download_hidden", "Hidden")
)
),
br(),
actionButton("trigger_shown", "I can trigger the visible button!"),
actionButton("trigger_hidden", "I can trigger the hidden button!")
),
server = function(input, output, session) {
output$download_shown <- downloadHandler(
filename = "sample.txt",
content = function(file) {
cat("I'm visible!\n", file = file)
}
)
output$download_hidden <- downloadHandler(
filename = "sample2.txt",
content = function(file) {
cat("I'm hidden!\n", file = file)
}
)
observeEvent(input$trigger_shown, {
session$sendCustomMessage(
"trigger-button",
list(button_id = "download_shown")
)
})
observeEvent(input$trigger_hidden, {
session$sendCustomMessage(
"trigger-button",
list(button_id = "download_hidden")
)
})
}
)
In the application, the two action buttons trigger their corresponding download button. Triggering the visible download button causes a correct download of the file sample.txt. Triggering the hidden download button causes a download of an HTML file, the webpage, instead of the sample2.txt file. Furthermore, if you inspect the HTML generated you can see that the download_hidden download button has an href attribute with no target.
Is there anything in the HTML spec that dictates a hidden element
cannot have an href target? This seems highly unlikely and none of my
searching has turned up any -one or -thing confirming this.
Internally does Shiny ignore elements which are hidden?
In the mean time, does anyone have a suggestion for hiding the button
without using hidden or display: none;?
Thank you in advance.
display:none; causes any element to be not rendered. Therefore, it will not take any space in the document. Therefore, it will not receive any (real) pointer-events.
I would not even count on its ability to receive programmatically triggered pointer events, as I expect at least a few major browsers to interfere, in the name of general browsing safety principles.
If you want your element to be a valid target for user interaction (real or programmatic), I suggest using...
opacity: .01;
...on it. This way it will be rendered. If you don't want it to occupy any space in the content flow, consider applying position:absolute to it.
I have discovered the solution thanks to this issue over on GitHub.
Shiny, by default, suspends objects which are hidden. So, by hiding a downloadButton the corresponding downloadHandler is suspended. I am still unsure how Shiny uses downloadHandler to register a download, but however the process works, it is not triggered if, as I said, the corresponding downloadButton is hidden.
The solution is to use the outputOptions function provided by Shiny. From the help page for outputOptions,
suspendWhenHidden, when TRUE (the default), the output object will be suspended (not execute) when it is hidden on the web page. When FALSE, the output object will not suspend when hidden, and if it was already hidden and suspended, then it will resume immediately.
By specifying suspendWhenHidden = FALSE after defining the downloadHandler we can prevent the href problem described in the original question.
Below is a revised, working version of the small Shiny application included in the original question.
shinyApp(
ui = fluidPage(
tags$head(
tags$style(HTML(".hide { display: none; }")),
tags$script(HTML('
Shiny.addCustomMessageHandler("trigger-button", function(message) {
document.getElementById(message.button_id).click();
});
'))
),
div(
class = "disable",
downloadButton("download_shown", "Shown"),
div(
class = "hide",
downloadButton("download_hidden", "Hidden")
)
),
br(),
actionButton("trigger_shown", "I can trigger the visible button!"),
actionButton("trigger_hidden", "I can trigger the hidden button!")
),
server = function(input, output, session) {
output$download_shown <- downloadHandler(
filename = "sample.txt",
content = function(file) {
cat("I'm visible!\n", file = file)
}
)
outputOptions(output, "download_shown", suspendWhenHidden = FALSE)
output$download_hidden <- downloadHandler(
filename = "sample2.txt",
content = function(file) {
cat("I'm hidden!\n", file = file)
}
)
outputOptions(output, "download_hidden", suspendWhenHidden = FALSE)
observeEvent(input$trigger_shown, {
session$sendCustomMessage(
"trigger-button",
list(button_id = "download_shown")
)
})
observeEvent(input$trigger_hidden, {
session$sendCustomMessage(
"trigger-button",
list(button_id = "download_hidden")
)
})
}
)
Remember to place calls to outputOptions after assigning corresponding reactive expressions to output, otherwise outputOptions will raise an error.
Related
On a web page I wish to display an element which depends on the state of some JavaScript. State like in a state machine. Currently the possible states are these (but I may add more):
input: display some input elements for the user to set. The user can click a button to start some JavaScript processing and move to the working state.
working: display a progress bar informing the user that the script is running. The user can cancel the computation (moving back to the input state) or the computation can end (moving to either the result or error state).
result: display the computation result. The user can go back to input with a button.
error: display the error. The user can go back to input with a button.
The JavaScript part is ready and working, but I'm unsure how to do this in HTML + CSS.
Current solution and its issue
Currently I've been doing it with classes: I set a class to a common ancestor element with the same name of the state and I display the right elements based on it. Something like this:
const parent=document.querySelector("#parent");
let timer=null;
function input(){
parent.classList.remove("working","result","error");
parent.classList.add("input");
}
function run(){
parent.classList.remove("input");
parent.classList.add("working");
timer=setTimeout(result,1500)
}
function stop(){
clearTimeout(timer);
input();
}
function result(){
parent.classList.remove("working");
if(Math.random()>0.5){parent.classList.add("result");}
else{parent.classList.add("error");}
}
input();
#input{display:none;}
#working{display:none;}
#result{display:none;}
#error{display:none;}
#parent.input #input{display:block;}
#parent.working #working{display:block;}
#parent.result #result{display:block;}
#parent.error #error{display:block;}
<div id="parent">
<div id="input">INPUT. RUN</div>
<div id="working">WORKING. STOP</div>
<div id="result">RESULT. RESTART</div>
<div id="error">ERROR. RESTART</div>
</div>
This solution works but it feels unstable: in theory it would be possible for the parent element to have no classes (in which case nothing is displayed) or multiple ones (in which case you'd see multiple states at once). This shouldn't happen, but the only thing preventing it is the correctness of my script.
Question
Are there better ways to implement this idea of states, so that the HTML elements can't end up in inconsistent states?
Let’s consider the role which HTML plays in a state machine on the web. A machine has moving parts, it is dynamic, so the core of any machine on the web must be implemented in Javascript. HTML is useful only to provide the interface between the user and the machine. It’s a subtle distinction but it fundamentally changes the way you write it.
Have you ever used React? React provides the framework to create entire web applications as “state machines”. React’s mantra is “UI is a function of state”. In a React app, you have a single variable which contains the current state, rendering code which builds the UI based on the state, and core code (mostly event handlers) which updates the state.
Even if you don’t want to build in React, you can use the same general idea:
keep the current state in a Javascript variable (typically you’d use an object, but in this case we only need a string)
write a rendering function which reads the state and then builds the appropriate HTML to represent that state
in the event handlers for your links, do any operations which are required, update the state and call the rendering function
let state = null
let timer = null
// core code
const input = () => {
state = 'input'
render()
}
const run = () => {
state = 'working'
render()
timer = setTimeout(result,1500)
}
const stop = () => {
clearTimeout(timer)
state = 'input'
render()
}
const result = () => {
if(Math.random()>0.5)
state = 'result'
else
state = 'error'
render()
}
// rendering code
const render = () => {
let x = state
switch(state) {
case 'input':
x += ' run'
break
case 'working':
x += ' stop'
break
case 'result':
x += ' restart'
break
case 'error':
x += ' restart'
break
}
document.getElementById('container').innerHTML = x
}
// initialisation code
state = 'input'
render()
<div id="container"></div>
TL;DR: Browser Autofill doesn't work as expected when inputs are in shadow DOMs, particularly noticed with the use of Web Components.
Clarification: The subject of this post is the HTML autocomplete attribute with a custom Web Component input. This is NOT referring to auto-completion of search terms.
Set up: First, let's suppose you want to create a vanilla HTML form to gather a user's name, address, and phone number. You would create a form element with a nested input element for each data point and a submit button. Straightforward and nothing unusual here.
Now, to improve the experience for your users you add the autocomplete attribute to each input with its associated value. I am sure you have seen and used this browser-supported feature before, and if you are like me, it is an expected convenience when filling out online forms for address, credit cards, and username/password.
Up to this point, we don't have any issues--everything is working as expected. With the autocomplete attributes added to the inputs, the browser recognizes that you are trying to fill out a form and a typical browser, such as Chrome, will use whatever user-provided data stored within the browser it can to help auto complete the inputs. In our case, granted you have information stored in your Chrome Preferences/Autofill/'Address and more', you will be given a pop-up list with your stored Address profiles to use to populate the form.
The Twist: If you change your native input to a Web Component with an open shadowDom--because perhaps you want a reusable input that has some validation and styling--the autocomplete no longer works.
Expected result:
I would expect the browser autocomplete feature to work as it normally does, such as, find, associate, and prefill inputs and not discriminate web component inputs that our in shadowDoms.
This is a known, lacking feature which is currently being worked on.
Follow https://groups.google.com/a/chromium.org/g/blink-dev/c/RY9leYMu5hI?pli=1 and https://bugs.chromium.org/p/chromium/issues/detail?id=649162
to stay up to date.
You can work around this by creating your input (and label) outside of the web component and including it via a slot.
const createInput = () => {
const input = document.createElement('input');
input.slot = 'input';
input.className = 'enterCodeInput';
input.name = 'code';
input.id = 'code';
input.autocomplete = 'one-time-code';
input.autocapitalize = 'none';
input.inputMode = 'numeric';
return input;
};
const createLabel = () => {
const label = document.createElement('label');
label.htmlFor = 'code';
label.className = 'enterCodeLabel';
label.innerHTML = `Enter Code`;
return label;
};
#customElement('foo')
class Foo extends LitElement {
#state()
protected _inputEl = createInput();
#state()
protected _labelEl = createLabel();
public connectedCallback() {
this._inputEl.addEventListener('input', this._handleCodeChange);
this.insertAdjacentElement('beforeend', this._labelEl);
this.insertAdjacentElement('beforeend', this._inputEl);
}
public disconnectedCallback() {
this._inputEl?.removeEventListener('input', this._handleCodeChange);
this._labelEl?.remove();
this._inputEl?.remove();
}
public render() {
return html`<form>
<slot name="label"></slot>
<slot name="input"></slot>
</form>`;
}
protected _handleCodeChange = (e: Event) => {
// Do something
};
}
You can style the input and label using the ::slotted pseudo-selector.
css`
::slotted(.enterCodeLabel) {}
::slotted(.enterCodeInput) {}
::slotted(.enterCodeInput:focus) {}
`
I have tried to implement the solution proposed here:
Not following URL Path Protocol
to separate all my plots in different files.
So basically I have radio buttons and based on user choice a different html file is loaded:
else if (input$chap == "4" & input$cat == "2") {
output$uiStub <- renderUI(tagList( # a single-output stub ui basically lets you
fluidPage( # move the ui into the server function
fluidRow(
column(12,
includeHTML("./html/mediapar.html")
)
),
uiOutput("pageStub") # loaded server code should render the
) # rest of the page to this output$
))
}
My problem is that every time a different file refresh (only one part of the page) the focus of page is lost and user has to scroll down again and again to get to the end of page where the choice can be made again and the plot shown.
fluidRow( style = "background-color:#FFFAFA00;",
box(
width = 12,
solidHeader = TRUE,
header = TRUE,
background = NULL,
ui <- uiOutput("uiStub")
)
Is there any workaround for this situation?
Kind Regards
After creating a new page using browser.newPage() I load a file with page.evaluateOnNewDocument() that consists of dynamically created settings for navigator (like a custom userAgent, language and webdriver's values), later with page.click() I click an element on that page, that has the target attribute set to "_blank", so it opens a new tab.
The question is, how can I transfer navigator's changes implemented to the page with clickable element, to the tab created with the click action? -> currently, once the element is clicked and a new tab is created it does not have navigator's changes implemented to the parent page.
<< EDIT>>
I have tested out the below and to my surprise it works for some cases, I assume it is due to some racing conditions. Perhaps somebody could put more light on it.
browser.on('targetcreated', async target => {
if (target.type() === "page") {
const page = await target.page();
await page.evaluateOnNewDocument(preloadFile);
await page.setUserAgent(data.userAgent);
await page.setViewport({
width: data.viewportWidth,
height: data.viewportHeight
});
}
});
You have two options. Either use page.on('popup', ...):
page.on('popup', async(page) => {
await page.evaluateOnNewDocument(...);
})
or remove the target="_blank" from the anchor tag before clicking to force opening in the same tab.
code below
var n = document.createElement("div");
Object.defineProperty(n, "id", {
get: function() {
window.location.href = homepage
}
})
I want to debug a page on a site, but when developer tools opened,
the code will bring me to homepage.(function get executed in Chrome)
How to get the div, then remove it to avoid jumping
Open the script in developer tools > Source (May be from homepage) & place debugger on the var n = document.createElement("div") line.
Now navigate to the page which you want to debug, the debugger will get activated.
Now replace the line (Or override the function Object.defineProperty(n, "id", .. ) and move step or disable debugger.