package xapi.elemental.api;
import elemental.client.Browser;
import elemental.dom.DocumentFragment;
import elemental.dom.Element;
import elemental.dom.Node;
import xapi.elemental.X_Elemental;
import xapi.ui.api.AttributeApplier;
import xapi.ui.api.ElementBuilder;
import xapi.ui.api.NodeBuilder;
import xapi.ui.api.Widget;
import xapi.util.X_Debug;
import xapi.util.X_String;
import xapi.util.impl.ImmutableProvider;
import java.util.function.BiFunction;
/**
* TODO: rename this to ElementalBuilder?
*/
public class PotentialNode <E extends Element> extends ElementBuilder<E> {
public class ApplyLiveAttribute implements AttributeApplier {
@Override
public void addAttribute(String name, String value) {
String is = getAttribute(name);
if (X_String.isEmpty(is)) {
setAttribute(name, value);
} else {
setAttribute(name, concat(name, is, value));
}
}
protected String concat(String name, String is, String value) {
return "class".equals(name) ?
X_Elemental.concatClass(is, value)
: is.concat(value);
}
@Override
public void setAttribute(String name, String value) {
getElement().setAttribute(name, value);
}
@Override
public String getAttribute(String name) {
return getElement().getAttribute(name);
}
@Override
public void removeAttribute(String name) {
getElement().removeAttribute(name);
}
}
private String tagName;
public PotentialNode() {
this(false);
}
public PotentialNode(boolean searchableChildren) {
super(searchableChildren);
setDefaultFactories();
}
public PotentialNode(String tagName) {
this();
setTagName(tagName);
}
public PotentialNode(String tagName, boolean searchableChildren) {
super(searchableChildren);
setDefaultFactories();
setTagName(tagName);
}
public PotentialNode(E element) {
super(false);
setDefaultFactories();
el = element;
onInitialize(el);
}
@Override
protected BiFunction<String, Boolean, NodeBuilder<E>> getCreator() {
return PotentialNode::new; // pick the constructor you like
}
@Override
protected void toHtml(Appendable out) {
if (tagName != null) {
// If we have a tagname, then we might expect our element to be addressable.
// In which case, we want to ensure it has an id
if (searchableChildren) {
ensureId();
}
}
super.toHtml(out);
}
@Override
protected AttributeApplier createAttributeApplier() {
return el == null ? new ApplyPendingAttribute() : new ApplyLiveAttribute();
}
public PotentialNode<E> createNode(String tagName) {
final NodeBuilder<E> child = newNode.apply(tagName, searchableChildren);
assert child instanceof PotentialNode :
"A potential node cannot have a factory which does not supply a new potential node" ;
return (PotentialNode<E>) child;
}
@Override
public PotentialNode<E> createChild(String tagName) {
final PotentialNode<E> child = createNode(tagName);
addChild(child, X_String.isEmpty(tagName)); // If we are a document fragment, we need to make new children the target element for future inserts
return child;
}
@Override
protected StyleApplier createStyleApplier() {
return new Styler();
}
@Override
protected E create(CharSequence csq) {
try {
E e = build(csq.toString());
return e;
} finally {
attributeApplier = new ImmutableProvider<>(new ApplyLiveAttribute());
}
}
protected E build(String html) {
boolean isMulti = children != null && children.hasSiblings();
if (isMulti) {
final DocumentFragment frag = X_Elemental.toFragment(sanitizeHtml(html).trim());
return compressFragment(frag, html);
}
return X_Elemental.toElement(sanitizeHtml(html));
}
protected E compressFragment(DocumentFragment frag, String html) {
final Node node = frag.getFirstChild();
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element e = (Element) frag.getFirstChild();
frag.removeChild(e);
e.appendChild(frag);
return (E)e;
} else {
assert false : "Cannot compress html into a single node; first child must be an element. Html:\n" + html;
throw X_Debug.recommendAssertions();
}
}
protected String sanitizeHtml(String html) {
return bodySanitizer.apply(html); // TODO: actually sanitize
}
@Override
public void append(Widget<E> child) {
getElement().appendChild(child.getElement());
}
@Override
protected NodeBuilder<E> wrapChars(CharSequence body) {
PotentialNode<E> node = new PotentialNode<>();
node.append(body);
return node;
}
@Override
protected CharSequence getCharsBefore() {
StringBuilder b = new StringBuilder();
if (tagName == null || tagName.isEmpty()) {
assert attributes.isEmpty() : "Cannot have attributes without a tagname";
} else {
b.append("<");
b.append(tagName);
appendAttributes(b);
if (isEmpty()) {
b.append("/");
}
b.append(">");
}
return b.length() == 0 ? EMPTY : b.toString();
}
@Override
protected boolean isEmpty() {
if (super.isEmpty()) {
switch(tagName.toLowerCase()) {
case "br":
case "img":
case "input":
// Any elements which should not be created with closing tags
return true;
}
}
return false;
}
private void appendAttributes(StringBuilder b) {
if (!attributes.isEmpty()) {
for (String attribute : attributes.keys()) {
b.append(" ").append(attribute).append("='");
String result = attributes.get(attribute).getElement();
b.append(sanitizeAttribute(result)).append("'");
}
}
}
protected String sanitizeAttribute(String result) {
return X_String.isEmpty(result) ? "" : result.replaceAll("'", "'");
}
@Override
protected CharSequence getCharsAfter(CharSequence self) {
if (tagName != null && !isEmpty()) {
return "</"+tagName+">";
}
return EMPTY;
}
/**
* @return the tagName
*/
public String getTagName() {
return tagName;
}
/**
* @param tagName the tagName to set
*/
public void setTagName(String tagName) {
this.tagName = tagName;
}
public String toSource() {
StringBuilder b = new StringBuilder();
toHtml(b);
return sanitizeHtml(b.toString());
}
@Override
public String toString() {
final E ele = getElement();
return ele == null ? "" : ele.getOuterHTML();
}
@Override
public StyleApplier getStyle() {
if (X_String.isEmpty(getTagName()) && children instanceof ElementBuilder) {
return ((ElementBuilder)children).getStyle();
}
return super.getStyle();
}
@Override
public ElementBuilder<E> setStyle(String name, String value) {
return super.setStyle(name, value);
}
class Styler extends StyleApplier {
@Override
protected void removeStyle(E element, String key) {
element.getStyle().removeProperty(key);
}
@Override
protected void setStyle(E element, String key, String value) {
element.getStyle().setProperty(key, value);
}
}
@Override
protected E findSelf(E parent) {
if (el != null) {
return el;
}
// First, look on the document. This is the fastest, IF we are attached
String id = getId();
if (id == null) {
return super.findSelf(parent);
}
el = (E) Browser.getDocument().getElementById(id);
if (el == null && parent != null) {
// If we aren't attached, fallback to the slower querySelector
el = (E) parent.querySelector("#"+id);
}
if (el != null) {
return el;
}
return super.findSelf(parent);
}
@Override
protected void startInitialize(E el) {
Browser.getDocument().getBody().appendChild(el);
super.startInitialize(el);
}
@Override
protected void finishInitialize(E el) {
final Element body = Browser.getDocument().getBody();
if (body == el.getParentElement()) {
body.removeChild(el);
}
super.finishInitialize(el);
}
}