> clickListener) {
setIcon(icon);
setText(text);
addClickListener(clickListener);
}
/**
* Sets the given string as the text content of this component.
*
* This method removes any existing text-content and replaces it with the
* given text.
*
* This method also sets or removes this button's theme=icon
* attribute based on whether this button contains only an icon after this
* operation or not.
*
* @param text
* the text content to set, may be null
to only
* remove existing text
*/
@Override
public void setText(String text) {
removeAll(getNonTextNodes());
if (text != null && !text.isEmpty()) {
getElement().appendChild(Element.createText(text));
}
updateThemeAttribute();
}
/**
* Sets the given component as the icon of this button.
*
* Even though you can use almost any component as an icon, some good
* options are {@code Icon} and {@link Image}.
*
* Use {@link #setIconAfterText(boolean)} to change the icon's position
* relative to the button's text content.
*
* This method also sets or removes this button's theme=icon
* attribute based on whether this button contains only an icon after this
* operation or not.
*
* @param icon
* component to be used as an icon, may be null
to
* only remove the current icon, can't be a text-node
*/
public void setIcon(Component icon) {
if (icon != null && icon.getElement().isTextNode()) {
throw new IllegalArgumentException(
"Text node can't be used as an icon.");
}
if (iconComponent != null) {
remove(iconComponent);
}
iconComponent = icon;
if (icon != null) {
add(icon);
updateIconSlot();
}
updateThemeAttribute();
}
/**
* Gets the component that is defined as the icon of this button.
*
* @return the icon of this button, or null
if the icon is not
* set
*/
public Component getIcon() {
return iconComponent;
}
/**
* Sets whether this button's icon should be positioned after it's text
* content or the other way around.
*
* At the element-level, this method determines whether to set
* {@code slot="prefix"} or {@code slot="suffix"} attribute to the icon.
*
* @param iconAfterText
* whether the icon should be positioned after the text content
* or not
*/
public void setIconAfterText(boolean iconAfterText) {
this.iconAfterText = iconAfterText;
if (iconComponent != null) {
updateIconSlot();
}
}
/**
* Gets whether this button's icon is positioned after it's text content or
* the other way around.
*
* @return true
if this button positions it's icon after it's
* text content, false
otherwise
*/
public boolean isIconAfterText() {
return iconAfterText;
}
/**
* Simulates a click on this button on the server side if it is enabled.
* Calling this method executes all registered click listeners on the server
* side, but does not execute possible client side registered listeners.
*
* @see #clickInClient()
*/
public void click() {
if (isEnabled()) {
fireEvent(new ClickEvent<>(this, false, 0, 0, 0, 0, 0, 0, false,
false, false, false));
}
}
/**
* Executes a click on this button at the client-side. Calling this method
* behaves the same as if the user would have clicked on the button.
*/
public void clickInClient() {
getElement().callJsFunction("click");
}
/**
* Adds the given components as children of this component.
*
* Note that using this method together with convenience methods, such as
* {@link #setText(String)} and {@link #setIcon(Component)}, may have
* unexpected results, e.g. in the order of child elements and the result of
* {@link #getText()}.
*
* @param components
* the components to add
*/
private void add(Component... components) {
assert components != null;
for (Component component : components) {
assert component != null;
getElement().appendChild(component.getElement());
}
}
/**
* Set the button to be input focused when the page loads.
*
* @param autofocus
* the boolean value to set
*/
public void setAutofocus(boolean autofocus) {
getElement().setProperty("autofocus", autofocus);
}
/**
* Get the state for the auto-focus property of the button.
*
* This property is not synchronized automatically from the client side, so
* the returned value may not be the same as in client side.
*
* @return the {@code autofocus} property from the button
*/
public boolean isAutofocus() {
return getElement().getProperty("autofocus", false);
}
/**
* Set the button so that it is disabled on click.
*
* Enabling the button needs to happen from the server.
*
* @param disableOnClick
* true to disable button immediately when clicked
*/
public void setDisableOnClick(boolean disableOnClick) {
this.disableOnClick = disableOnClick;
if (disableOnClick) {
getElement().setAttribute("disableOnClick", "true");
initDisableOnClick();
} else {
getElement().removeAttribute("disableOnClick");
}
}
/**
* Get if button is set to be disabled on click.
*
* @return {@code true} if button gets disabled on click, else {@code false}
*/
public boolean isDisableOnClick() {
return disableOnClick;
}
/**
* Initialize client side disabling so disabled if immediate on click even
* if server-side handling takes some time.
*/
private void initDisableOnClick() {
if (initDisableOnClick == null) {
initDisableOnClick = getElement().executeJs(
"window.Vaadin.Flow.button.initDisableOnClick($0)");
getElement().getNode()
.runWhenAttached(ui -> ui.beforeClientResponse(this,
executionContext -> this.initDisableOnClick = null));
}
}
@Override
public void setEnabled(boolean enabled) {
Focusable.super.setEnabled(enabled);
// Force updating the disabled state on the client
// When using disable on click, the client side will immediately
// run JS to disable the button. If the button is then disabled and
// re-enabled during the same round trip, Flow will not detect any
// changes and the client side button would not be enabled again.
getElement().executeJs("this.disabled = $0", !enabled);
}
private void updateIconSlot() {
iconComponent.getElement().setAttribute("slot",
iconAfterText ? "suffix" : "prefix");
}
/**
* Removes the given child components from this component.
*
* @param components
* The components to remove.
* @throws IllegalArgumentException
* if any of the components is not a child of this component.
*/
protected void remove(Component... components) {
for (Component component : components) {
if (getElement().equals(component.getElement().getParent())) {
component.getElement().removeAttribute("slot");
getElement().removeChild(component.getElement());
} else {
throw new IllegalArgumentException("The given component ("
+ component + ") is not a child of this component");
}
}
}
/**
* Removes all contents from this component except elements in
* {@code exclusion} array. This includes child components, text content as
* well as child elements that have been added directly to this component
* using the {@link Element} API.
*
* @see Button#removeAll()
*/
private void removeAll(Element... exclusion) {
Set toExclude = Stream.of(exclusion)
.collect(Collectors.toSet());
Predicate filter = toExclude::contains;
getElement().getChildren().filter(filter.negate())
.forEach(child -> child.removeAttribute("slot"));
getElement().removeAllChildren();
getElement().appendChild(exclusion);
}
private Element[] getNonTextNodes() {
return getElement().getChildren()
.filter(element -> !element.isTextNode())
.toArray(Element[]::new);
}
private void updateThemeAttribute() {
// Add theme attribute "icon" when the button contains only an icon to
// fully support themes like Lumo. This doesn't override explicitly set
// theme attribute.
long childCount = getElement().getChildren().filter(
el -> el.isTextNode() || !"vaadin-tooltip".equals(el.getTag()))
.count();
if (childCount == 1 && iconComponent != null) {
getThemeNames().add("icon");
} else {
getThemeNames().remove("icon");
}
}
@Override
protected void onAttach(AttachEvent attachEvent) {
if (isDisableOnClick()) {
initDisableOnClick();
}
}
}