package com.sksamuel.jqm4gwt.layout;
import java.util.ArrayList;
import java.util.List;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiChild;
import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.Widget;
import com.sksamuel.jqm4gwt.DataIcon;
import com.sksamuel.jqm4gwt.Empty;
import com.sksamuel.jqm4gwt.HasIconPos;
import com.sksamuel.jqm4gwt.HasInline;
import com.sksamuel.jqm4gwt.HasInset;
import com.sksamuel.jqm4gwt.HasMini;
import com.sksamuel.jqm4gwt.HasText;
import com.sksamuel.jqm4gwt.IconPos;
import com.sksamuel.jqm4gwt.JQMCommon;
import com.sksamuel.jqm4gwt.JQMContainer;
import com.sksamuel.jqm4gwt.button.JQMButton;
import com.sksamuel.jqm4gwt.html.CustomFlowPanel;
import com.sksamuel.jqm4gwt.html.Heading;
import com.sksamuel.jqm4gwt.layout.JQMCollapsibleEvent.CollapsibleState;
/**
* @author Stephen K Samuel samspade79@gmail.com 10 May 2011 00:04:18
* <br>
* A {@link JQMCollapsible} is a panel that shows a header and can reveal content
* once the header is expanded. This is similar to the GWT {@link DisclosurePanel}.
*
* <br> See <a href="http://demos.jquerymobile.com/1.4.5/collapsible/">Collapsible</a>
*/
public class JQMCollapsible extends JQMContainer implements HasText<JQMCollapsible>,
HasIconPos<JQMCollapsible>, HasMini<JQMCollapsible>, HasInset<JQMCollapsible>,
HasInline<JQMCollapsible> {
private final Heading header;
private CustomFlowPanel headerPanel;
private Element headingToggle;
private Element collapsibleContent;
private String contentStyleNames;
private boolean inline;
/**
* Creates a new {@link JQMCollapsible} with the no header text and
* preset to collapsed.
*/
public JQMCollapsible() {
this(null, true);
}
/**
* Creates a new {@link JQMCollapsible} with the given header text and
* preset to collapsed.
*/
public JQMCollapsible(String text) {
this(text, true);
}
/**
* Creates a new {@link JQMCollapsible} with the given header text and
* collapsed if param collapsed is true.
* <br>
* The created header will use a h3 element.
*
* @param collapsed if true then the {@link JQMCollapsible} will be collapsed
* by default, if false it will be open by default
*/
public JQMCollapsible(String text, boolean collapsed) {
this(text, 3, collapsed);
}
/**
* Creates a new {@link JQMCollapsible} with the given header text and
* collapsed if param collapsed is true.
* <br>
* The created header will use a <hN> element where N is determined by the param headerN.
* <br>
* Once the {@link JQMCollapsible} has been created it is not possible to
* change the <hN> tag used for the header.
*/
public JQMCollapsible(String text, int headerN, boolean collapsed) {
header = new Heading(headerN);
add(header);
setRole("collapsible");
setCollapsed(collapsed);
setText(text);
}
public HandlerRegistration addCollapsibleHandler(JQMCollapsibleEvent.Handler handler) {
return addHandler(handler, JQMCollapsibleEvent.getType());
}
protected void onExpanded() {
}
protected void onCollapsed() {
}
/**
* @param eventTarget - The DOM element that initiated the event,
* see <a href="http://api.jquery.com/event.target/">event.target</a>
*/
protected void doExpanded(Element eventTarget) {
onExpanded();
JQMCollapsibleEvent.fire(this, CollapsibleState.EXPANDED, eventTarget);
}
/**
* @param eventTarget - The DOM element that initiated the event,
* see <a href="http://api.jquery.com/event.target/">event.target</a>
*/
protected void doCollapsed(Element eventTarget) {
onCollapsed();
JQMCollapsibleEvent.fire(this, CollapsibleState.COLLAPSED, eventTarget);
}
private static native void bindLifecycleEvents(JQMCollapsible collap, Element collapElt) /*-{
var p = $wnd.$(collapElt);
p.on("collapsibleexpand", function(event, ui) {
collap.@com.sksamuel.jqm4gwt.layout.JQMCollapsible::doExpanded(Lcom/google/gwt/dom/client/Element;)(event.target);
});
p.on("collapsiblecollapse", function(event, ui) {
collap.@com.sksamuel.jqm4gwt.layout.JQMCollapsible::doCollapsed(Lcom/google/gwt/dom/client/Element;)(event.target);
});
}-*/;
private static native void unbindLifecycleEvents(Element collapElt) /*-{
var p = $wnd.$(collapElt);
p.off("collapsiblecollapse");
p.off("collapsibleexpand");
}-*/;
private static native void bindCreated(Element elt, JQMCollapsible co) /*-{
$wnd.$(elt).on( 'collapsiblecreate', function( event, ui ) {
co.@com.sksamuel.jqm4gwt.layout.JQMCollapsible::created()();
});
}-*/;
private static native void unbindCreated(Element elt) /*-{
$wnd.$(elt).off( 'collapsiblecreate' );
}-*/;
/**
* Unfortunately it's not called in case of manual JQMContext.render(),
* though widget is getting created and enhanced.
*/
private void created() {
findSubParts();
}
private static native boolean isInstance(Element elt) /*-{
var i = $wnd.$(elt).collapsible("instance");
if (i) return true;
else return false;
}-*/;
private void findSubParts() {
Boolean isInstance = null;
if (headingToggle == null) {
if (isInstance == null) isInstance = isInstance(getElement());
if (isInstance) {
headingToggle = JQMCommon.findFirst(header.getElement(), ".ui-collapsible-heading-toggle.ui-btn");
if (headingToggle != null) {
JQMCommon.setInlineEx(headingToggle, inline, JQMCommon.STYLE_UI_BTN_INLINE);
}
}
}
if (collapsibleContent == null) {
if (isInstance == null) isInstance = isInstance(getElement());
if (isInstance) {
collapsibleContent = JQMCommon.findFirst(getElement(), ".ui-collapsible-content");
if (collapsibleContent != null) JQMCommon.addStyleNames(collapsibleContent, contentStyleNames);
}
}
}
@Override
protected void onLoad() {
findSubParts(); // should be before super.onLoad(), because for example
// JQMContext.getWidgetDefaults() may start setting some properties immediately
super.onLoad();
Element elt = getElement();
bindCreated(elt, this);
bindLifecycleEvents(this, elt);
}
@Override
protected void onUnload() {
Element elt = getElement();
unbindLifecycleEvents(elt);
unbindCreated(elt);
super.onUnload();
}
@Override
public void setTheme(String themeName) {
super.setTheme(themeName);
if (headingToggle != null) {
JQMButton.setTheme(headingToggle, themeName);
}
}
/** Sets the header button as inline block. */
@Override
public void setInline(boolean value) {
inline = value;
if (headingToggle != null) {
JQMCommon.setInlineEx(headingToggle, value, JQMCommon.STYLE_UI_BTN_INLINE);
}
}
@Override
public boolean isInline() {
if (headingToggle == null) {
return inline;
} else {
boolean v = JQMCommon.isInlineEx(headingToggle, JQMCommon.STYLE_UI_BTN_INLINE);
inline = v;
return inline;
}
}
@Override
public JQMCollapsible withInline(boolean inline) {
setInline(inline);
return this;
}
public String getContentTheme() {
return getAttribute("data-content-theme");
}
public void setContentTheme(String themeName) {
setAttribute("data-content-theme", themeName);
if (collapsibleContent != null) {
String s = Empty.is(themeName) ? JQMCommon.THEME_INHERIT : themeName;
JQMCommon.setThemeEx(collapsibleContent, s, JQMCommon.STYLE_UI_BODY);
}
}
public JQMCollapsible withContentTheme(String themeName) {
setContentTheme(themeName);
return this;
}
public String getContentStyleNames() {
return contentStyleNames;
}
public void setContentStyleNames(String value) {
if (contentStyleNames == value || contentStyleNames != null && contentStyleNames.equals(value)) return;
if (collapsibleContent != null) JQMCommon.removeStyleNames(collapsibleContent, contentStyleNames);
contentStyleNames = value;
if (collapsibleContent != null) JQMCommon.addStyleNames(collapsibleContent, contentStyleNames);
}
public JQMCollapsible withContentStyleNames(String value) {
setContentStyleNames(value);
return this;
}
/**
* The same as setContentStyleNames(), mostly just for UiBinder to be able define: addContentStyleNames="..."
*/
public void setAddContentStyleNames(String value) {
setContentStyleNames(value);
}
/**
* Add a widget to the content part of this {@link JQMCollapsible} instance.
*/
@Override
@UiChild(tagname="widget")
public void add(Widget widget) {
super.add(widget);
}
@UiChild(tagname="headerWidget")
public void addHeaderWidget(Widget w) {
if (w == null) return;
if (headerPanel == null) {
header.getElement().setInnerHTML(null);
headerPanel = new CustomFlowPanel(header.getElement());
add(headerPanel);
}
headerPanel.add(w);
}
/**
* Removes all Widgets from the content part of this {@link JQMCollapsible} instance.
*/
@Override
public void clear() {
super.clear();
}
public DataIcon getCollapsedIcon() {
return DataIcon.fromJqmValue(getAttribute("data-collapsed-icon"));
}
public void setCollapsedIcon(DataIcon icon) {
setAttribute("data-collapsed-icon", icon != null ? icon.getJqmValue() : null);
}
public DataIcon getExpandedIcon() {
return DataIcon.fromJqmValue(getAttribute("data-expanded-icon"));
}
public void setExpandedIcon(DataIcon icon) {
setAttribute("data-expanded-icon", icon != null ? icon.getJqmValue() : null);
}
public JQMCollapsible removeCollapsedIcon() {
removeAttribute("data-collapsed-icon");
return this;
}
public JQMCollapsible removeExpandedIcon() {
removeAttribute("data-expanded-icon");
return this;
}
@Override
public IconPos getIconPos() {
return JQMCommon.getIconPos(this);
}
/**
* Returns the text on the header element
*/
@Override
public String getText() {
return header.getText();
}
/**
* Returns true if this {@link JQMCollapsible} is currently collapsed.
*/
public boolean isCollapsed() {
boolean v = JQMCommon.hasStyle(this, "ui-collapsible-collapsed");
if (v) return v;
if (!isInstance(getElement())) {
String s = getAttribute("data-collapsed");
if (Empty.is(s)) return true; // true by default, see https://api.jquerymobile.com/collapsible/#option-collapsed
if ("false".equalsIgnoreCase(s)) return false;
else return true;
}
return false;
}
/**
* Programmatically set the collapsed state of this widget.
*/
public void setCollapsed(boolean collapsed) {
if (collapsed) {
removeAttribute("data-collapsed");
if (isInstance(getElement())) collapse();
} else {
setAttribute("data-collapsed", "false");
if (isInstance(getElement())) expand();
}
}
/**
* Programmatically set the collapsed state of this widget.
*/
public JQMCollapsible withCollapsed(boolean collapsed) {
setCollapsed(collapsed);
return this;
}
@Override
public boolean isInset() {
return getAttributeBoolean("data-inset");
}
@Override
public boolean isMini() {
return JQMCommon.isMini(this);
}
/**
* Removes a widget from the content part of this {@link JQMCollapsible}
* instance.
*
* @return true if the widget was removed
*/
@Override
public boolean remove(Widget widget) {
return super.remove(widget);
}
/**
* Sets the position of the icon. If you desire an icon only button then
* set the position to IconPos.NOTEXT
*/
@Override
public void setIconPos(IconPos pos) {
JQMCommon.setIconPos(this, pos);
}
/**
* Sets the position of the icon. If you desire an icon only button then
* set the position to IconPos.NOTEXT
*/
@Override
public JQMCollapsible withIconPos(IconPos pos) {
setIconPos(pos);
return this;
}
@Override
public void setInset(boolean inset) {
setAttribute("data-inset", String.valueOf(inset));
}
@Override
public JQMCollapsible withInset(boolean inset) {
setInset(inset);
return this;
}
/**
* If set to true then renders a smaller version of the standard-sized element.
*/
@Override
public void setMini(boolean mini) {
JQMCommon.setMini(this, mini);
}
/**
* If set to true then renders a smaller version of the standard-sized element.
*/
@Override
public JQMCollapsible withMini(boolean mini) {
setMini(mini);
return this;
}
/**
* Sets the text on the header element
*/
@Override
public void setText(String text) {
if (headerPanel != null) {
super.remove(headerPanel);
headerPanel = null;
}
header.setText(text);
}
@Override
public JQMCollapsible withText(String text) {
setText(text);
return this;
}
public void expand() {
execExpand(getElement());
}
public void collapse() {
execCollapse(getElement());
}
private static native void execExpand(Element elt) /*-{
$wnd.$(elt).collapsible("expand");
}-*/;
private static native void execCollapse(Element elt) /*-{
$wnd.$(elt).collapsible("collapse");
}-*/;
public boolean isHeaderChild(Widget w) {
if (w == null || headerPanel == null) return false;
w = w.getParent();
while (w != null) {
if (headerPanel == w) return true;
w = w.getParent();
}
return false;
}
/**
* @return - if widget is child of some collapsible's header then returns that collapsible, otherwise null
*/
public static JQMCollapsible isCollapsibleHeaderChild(Widget w) {
if (w == null) return null;
List<Widget> parentChain = null;
w = w.getParent();
while (w != null) {
if (w instanceof JQMCollapsible) {
JQMCollapsible co = (JQMCollapsible) w;
if (co.headerPanel != null && parentChain != null && parentChain.contains(co.headerPanel)) {
return co;
} else {
return null;
}
}
if (parentChain == null) parentChain = new ArrayList<>();
parentChain.add(w);
w = w.getParent();
}
return null;
}
/** Needed for header widgets to prevent expand/collapse on their clicks. */
public void discardHeaderClick(ClickEvent event) {
if (event == null) return;
// Example: we use radioset on collapsible header, so stopPropagation() is needed
// to suppress collapsible open/close behavior.
// But preventDefault() is not needed, otherwise radios won't switch.
// event.preventDefault(); // For example, clicked anchors will not take the browser to a new URL
event.stopPropagation();
makeHeaderInactive(header.getElement());
}
/** On iOS if placed on header button is pressed then header remains in "pressed" state. */
private static native void makeHeaderInactive(Element header) /*-{
$wnd.$(header).find("a.ui-btn").first().removeClass($wnd.$.mobile.activeBtnClass);
}-*/;
}