org.htmlunit.html.HtmlElement Maven / Gradle / Ivy
Show all versions of xlt Show documentation
/*
* Copyright (c) 2002-2024 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.html;
import static org.htmlunit.BrowserVersionFeatures.FORM_FORM_ATTRIBUTE_SUPPORTED;
import static org.htmlunit.BrowserVersionFeatures.HTMLELEMENT_DETACH_ACTIVE_TRIGGERS_NO_KEYUP_EVENT;
import static org.htmlunit.BrowserVersionFeatures.HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT;
import static org.htmlunit.BrowserVersionFeatures.KEYBOARD_EVENT_SPECIAL_KEYPRESS;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.htmlunit.BrowserVersion;
import org.htmlunit.ElementNotFoundException;
import org.htmlunit.Page;
import org.htmlunit.ScriptResult;
import org.htmlunit.SgmlPage;
import org.htmlunit.WebAssert;
import org.htmlunit.WebClient;
import org.htmlunit.html.impl.SelectableTextInput;
import org.htmlunit.javascript.HtmlUnitScriptable;
import org.htmlunit.javascript.host.dom.Document;
import org.htmlunit.javascript.host.dom.MutationObserver;
import org.htmlunit.javascript.host.event.Event;
import org.htmlunit.javascript.host.event.EventTarget;
import org.htmlunit.javascript.host.event.KeyboardEvent;
import org.htmlunit.javascript.host.html.HTMLDocument;
import org.htmlunit.javascript.host.html.HTMLElement;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
/**
* An abstract wrapper for HTML elements.
*
* @author Mike Bowler
* @author Mike J. Bresnahan
* @author David K. Taylor
* @author Christian Sell
* @author David D. Kilzer
* @author Mike Gallaher
* @author Denis N. Antonioli
* @author Marc Guillemot
* @author Ahmed Ashour
* @author Daniel Gredler
* @author Dmitri Zoubkov
* @author Sudhan Moghe
* @author Ronald Brill
* @author Frank Danek
* @author Ronny Shapiro
*/
public abstract class HtmlElement extends DomElement {
/**
* Enum for the different display styles.
*/
public enum DisplayStyle {
/** Empty string. */
EMPTY(""),
/** none. */
NONE("none"),
/** block. */
BLOCK("block"),
/** contents. */
CONTENTS("contents"),
/** inline. */
INLINE("inline"),
/** inline-block. */
INLINE_BLOCK("inline-block"),
/** list-item. */
LIST_ITEM("list-item"),
/** table. */
TABLE("table"),
/** table-cell. */
TABLE_CELL("table-cell"),
/** table-column. */
TABLE_COLUMN("table-column"),
/** table-column-group. */
TABLE_COLUMN_GROUP("table-column-group"),
/** table-row. */
TABLE_ROW("table-row"),
/** table-row-group. */
TABLE_ROW_GROUP("table-row-group"),
/** table-header-group. */
TABLE_HEADER_GROUP("table-header-group"),
/** table-footer-group. */
TABLE_FOOTER_GROUP("table-footer-group"),
/** table-caption. */
TABLE_CAPTION("table-caption"),
/** ruby. */
RUBY("ruby"),
/** ruby-base. */
RUBY_BASE("ruby-base"),
/** ruby-text-container. */
RUBY_TEXT("ruby-text"),
/** ruby-text-container. */
RUBY_TEXT_CONTAINER("ruby-text-container");
private final String value_;
DisplayStyle(final String value) {
value_ = value;
}
/**
* The string used from js.
* @return the value as string
*/
public String value() {
return value_;
}
}
/**
* Constant indicating that a tab index value is out of bounds (less than 0
or greater
* than 32767
).
*
* @see #getTabIndex()
*/
public static final Short TAB_INDEX_OUT_OF_BOUNDS = Short.valueOf(Short.MIN_VALUE);
/** Constant 'required'. */
protected static final String ATTRIBUTE_REQUIRED = "required";
/** Constant 'checked'. */
protected static final String ATTRIBUTE_CHECKED = "checked";
/** The listeners which are to be notified of attribute changes. */
private final List attributeListeners_ = new ArrayList<>();
/** The owning form for lost form children. */
private HtmlForm owningForm_;
private boolean shiftPressed_;
private boolean ctrlPressed_;
private boolean altPressed_;
/**
* Creates an instance.
*
* @param qualifiedName the qualified name of the element type to instantiate
* @param page the page that contains this element
* @param attributes a map ready initialized with the attributes for this element, or
* {@code null}. The map will be stored as is, not copied.
*/
protected HtmlElement(final String qualifiedName, final SgmlPage page,
final Map attributes) {
this(Html.XHTML_NAMESPACE, qualifiedName, page, attributes);
}
/**
* Creates an instance of a DOM element that can have a namespace.
*
* @param namespaceURI the URI that identifies an XML namespace
* @param qualifiedName the qualified name of the element type to instantiate
* @param page the page that contains this element
* @param attributes a map ready initialized with the attributes for this element, or
* {@code null}. The map will be stored as is, not copied.
*/
protected HtmlElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
final Map attributes) {
super(namespaceURI, qualifiedName, page, attributes);
}
/**
* {@inheritDoc}
*/
@Override
protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
final String attributeValue, final boolean notifyAttributeChangeListeners,
final boolean notifyMutationObservers) {
// TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
if (null == getHtmlPageOrNull()) {
super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
notifyMutationObservers);
return;
}
final String oldAttributeValue = getAttribute(qualifiedName);
final HtmlPage htmlPage = (HtmlPage) getPage();
final boolean mappedElement = isAttachedToPage()
&& HtmlPage.isMappedElement(htmlPage, qualifiedName);
if (mappedElement) {
// cast is save here because isMappedElement checks for HtmlPage
htmlPage.removeMappedElement(this);
}
final HtmlAttributeChangeEvent event;
if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
event = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
}
else {
event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
}
super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
notifyMutationObservers);
if (notifyAttributeChangeListeners) {
notifyAttributeChangeListeners(event, this, oldAttributeValue, notifyMutationObservers);
}
fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
}
/**
* Recursively notifies all {@link HtmlAttributeChangeListener}s.
* @param event the event
* @param element the element
* @param oldAttributeValue the old attribute value
* @param notifyMutationObservers whether to notify {@link MutationObserver}s or not
*/
protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
final List listeners = element.attributeListeners_;
if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
synchronized (listeners) {
for (final HtmlAttributeChangeListener listener : listeners) {
if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
listener.attributeAdded(event);
}
}
}
}
else {
synchronized (listeners) {
for (final HtmlAttributeChangeListener listener : listeners) {
if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
listener.attributeReplaced(event);
}
}
}
}
final DomNode parentNode = element.getParentNode();
if (parentNode instanceof HtmlElement) {
notifyAttributeChangeListeners(event, (HtmlElement) parentNode, oldAttributeValue, notifyMutationObservers);
}
}
private void fireAttributeChangeImpl(final HtmlAttributeChangeEvent event,
final HtmlPage htmlPage, final boolean mappedElement, final String oldAttributeValue) {
if (mappedElement) {
htmlPage.addMappedElement(this);
}
if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
fireHtmlAttributeAdded(event);
htmlPage.fireHtmlAttributeAdded(event);
}
else {
fireHtmlAttributeReplaced(event);
htmlPage.fireHtmlAttributeReplaced(event);
}
}
/**
* Sets the specified attribute. This method may be overridden by subclasses
* which are interested in specific attribute value changes, but such methods must
* invoke super.setAttributeNode()
, and should consider the value of the
* cloning
parameter when deciding whether or not to execute custom logic.
*
* @param attribute the attribute to set
* @return {@inheritDoc}
*/
@Override
public Attr setAttributeNode(final Attr attribute) {
final String qualifiedName = attribute.getName();
final String oldAttributeValue = getAttribute(qualifiedName);
final HtmlPage htmlPage = (HtmlPage) getPage();
final boolean mappedElement = isAttachedToPage()
&& HtmlPage.isMappedElement(htmlPage, qualifiedName);
if (mappedElement) {
// cast is save here because isMappedElement checks for HtmlPage
htmlPage.removeMappedElement(this);
}
final HtmlAttributeChangeEvent event;
if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
event = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
}
else {
event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
}
notifyAttributeChangeListeners(event, this, oldAttributeValue, true);
final Attr result = super.setAttributeNode(attribute);
fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
return result;
}
/**
* Removes an attribute specified by name from this element.
* @param attributeName the attribute attributeName
*/
@Override
public void removeAttribute(final String attributeName) {
final String value = getAttribute(attributeName);
if (ATTRIBUTE_NOT_DEFINED == value) {
return;
}
final HtmlPage htmlPage = getHtmlPageOrNull();
if (htmlPage != null) {
htmlPage.removeMappedElement(this);
}
super.removeAttribute(attributeName);
if (htmlPage != null) {
htmlPage.addMappedElement(this);
final HtmlAttributeChangeEvent event = new HtmlAttributeChangeEvent(this, attributeName, value);
fireHtmlAttributeRemoved(event);
htmlPage.fireHtmlAttributeRemoved(event);
}
}
/**
* Support for reporting HTML attribute changes. This method can be called when an attribute
* has been added and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
* registered {@link HtmlAttributeChangeListener}s.
*
* Note that this method recursively calls this element's parent's
* {@link #fireHtmlAttributeAdded(HtmlAttributeChangeEvent)} method.
*
* @param event the event
* @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
*/
protected void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
final DomNode parentNode = getParentNode();
if (parentNode instanceof HtmlElement) {
((HtmlElement) parentNode).fireHtmlAttributeAdded(event);
}
}
/**
* Support for reporting HTML attribute changes. This method can be called when an attribute
* has been replaced and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
* registered {@link HtmlAttributeChangeListener}s.
*
* Note that this method recursively calls this element's parent's
* {@link #fireHtmlAttributeReplaced(HtmlAttributeChangeEvent)} method.
*
* @param event the event
* @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
*/
protected void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
final DomNode parentNode = getParentNode();
if (parentNode instanceof HtmlElement) {
((HtmlElement) parentNode).fireHtmlAttributeReplaced(event);
}
}
/**
* Support for reporting HTML attribute changes. This method can be called when an attribute
* has been removed and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
* registered {@link HtmlAttributeChangeListener}s.
*
* Note that this method recursively calls this element's parent's
* {@link #fireHtmlAttributeRemoved(HtmlAttributeChangeEvent)} method.
*
* @param event the event
* @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
*/
protected void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
synchronized (attributeListeners_) {
for (final HtmlAttributeChangeListener listener : attributeListeners_) {
listener.attributeRemoved(event);
}
}
final DomNode parentNode = getParentNode();
if (parentNode instanceof HtmlElement) {
((HtmlElement) parentNode).fireHtmlAttributeRemoved(event);
}
}
/**
* @return the same value as returned by {@link #getTagName()}
*/
@Override
public String getNodeName() {
final String prefix = getPrefix();
if (prefix != null) {
// create string builder only if needed (performance)
final StringBuilder name = new StringBuilder(prefix.toLowerCase(Locale.ROOT))
.append(':')
.append(getLocalName().toLowerCase(Locale.ROOT));
return name.toString();
}
return getLocalName().toLowerCase(Locale.ROOT);
}
/**
* Returns this element's tab index, if it has one. If the tab index is outside of the
* valid range (less than 0
or greater than 32767
), this method
* returns {@link #TAB_INDEX_OUT_OF_BOUNDS}. If this element does not have
* a tab index, or its tab index is otherwise invalid, this method returns {@code null}.
*
* @return this element's tab index
*/
public Short getTabIndex() {
final String index = getAttributeDirect("tabindex");
if (index == null || index.isEmpty()) {
return null;
}
try {
final long l = Long.parseLong(index);
if (l >= 0 && l <= Short.MAX_VALUE) {
return Short.valueOf((short) l);
}
return TAB_INDEX_OUT_OF_BOUNDS;
}
catch (final NumberFormatException e) {
return null;
}
}
/**
* Returns the first element with the specified tag name that is an ancestor to this element, or
* {@code null} if no such element is found.
* @param tagName the name of the tag searched (case insensitive)
* @return the first element with the specified tag name that is an ancestor to this element
*/
public HtmlElement getEnclosingElement(final String tagName) {
final String tagNameLC = tagName.toLowerCase(Locale.ROOT);
for (DomNode currentNode = getParentNode(); currentNode != null; currentNode = currentNode.getParentNode()) {
if (currentNode instanceof HtmlElement && currentNode.getNodeName().equals(tagNameLC)) {
return (HtmlElement) currentNode;
}
}
return null;
}
/**
* Returns the form which contains this element, or {@code null} if this element is not inside
* of a form.
* @return the form which contains this element
*/
public HtmlForm getEnclosingForm() {
final BrowserVersion browserVersion = getPage().getWebClient().getBrowserVersion();
if (browserVersion.hasFeature(FORM_FORM_ATTRIBUTE_SUPPORTED)) {
final String formId = getAttribute("form");
if (ATTRIBUTE_NOT_DEFINED != formId) {
final Element formById = getPage().getElementById(formId);
if (formById instanceof HtmlForm) {
return (HtmlForm) formById;
}
return null;
}
}
if (owningForm_ != null) {
return owningForm_;
}
return (HtmlForm) getEnclosingElement("form");
}
/**
* Returns the form which contains this element. If this element is not inside a form, this method
* throws an {@link IllegalStateException}.
* @return the form which contains this element
*/
public HtmlForm getEnclosingFormOrDie() {
final HtmlForm form = getEnclosingForm();
if (form == null) {
throw new IllegalStateException("Element is not contained within a form: " + this);
}
return form;
}
/**
* Simulates typing the specified text while this element has focus.
* Note that for some elements, typing '\n' submits the enclosed form.
* @param text the text you with to simulate typing
* @exception IOException If an IO error occurs
*/
public void type(final String text) throws IOException {
for (final char ch : text.toCharArray()) {
type(ch);
}
}
/**
* Simulates typing the specified character while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc. Note also that for some elements, typing '\n'
* submits the enclosed form.
*
* @param c the character you wish to simulate typing
* @return the page that occupies this window after typing
* @exception IOException if an IO error occurs
*/
public Page type(final char c) throws IOException {
return type(c, true);
}
/**
* Simulates typing the specified character while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc. Note also that for some elements, typing '\n'
* submits the enclosed form.
*
* @param c the character you wish to simulate typing
* @param lastType is this the last character to type
* @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
* @exception IOException if an IO error occurs
*/
private Page type(final char c, final boolean lastType)
throws IOException {
if (isDisabledElementAndDisabled()) {
return getPage();
}
// make enclosing window the current one
getPage().getWebClient().setCurrentWindow(getPage().getEnclosingWindow());
final HtmlPage page = (HtmlPage) getPage();
if (page.getFocusedElement() != this) {
focus();
}
final boolean isShiftNeeded = KeyboardEvent.isShiftNeeded(c, shiftPressed_);
final Event shiftDown;
final ScriptResult shiftDownResult;
if (isShiftNeeded) {
shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
true, ctrlPressed_, altPressed_);
shiftDownResult = fireEvent(shiftDown);
}
else {
shiftDown = null;
shiftDownResult = null;
}
final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
final ScriptResult keyDownResult = fireEvent(keyDown);
if (!keyDown.isAborted(keyDownResult)) {
final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
final ScriptResult keyPressResult = fireEvent(keyPress);
if ((shiftDown == null || !shiftDown.isAborted(shiftDownResult))
&& !keyPress.isAborted(keyPressResult)) {
doType(c, lastType);
}
}
final WebClient webClient = page.getWebClient();
if (this instanceof HtmlTextInput
|| this instanceof HtmlTextArea
|| this instanceof HtmlTelInput
|| this instanceof HtmlNumberInput
|| this instanceof HtmlSearchInput
|| this instanceof HtmlPasswordInput) {
fireEvent(new KeyboardEvent(this, Event.TYPE_INPUT, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_));
}
HtmlElement eventSource = this;
if (!isAttachedToPage()) {
final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
if (browserVersion.hasFeature(HTMLELEMENT_DETACH_ACTIVE_TRIGGERS_NO_KEYUP_EVENT)) {
eventSource = null;
}
else {
eventSource = page.getBody();
}
}
if (eventSource != null) {
final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c,
shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
eventSource.fireEvent(keyUp);
if (isShiftNeeded) {
final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP,
KeyboardEvent.DOM_VK_SHIFT,
false, ctrlPressed_, altPressed_);
eventSource.fireEvent(shiftUp);
}
}
final HtmlForm form = getEnclosingForm();
if (form != null && c == '\n' && isSubmittableByEnter()) {
final HtmlSubmitInput submit = form.getFirstByXPath(".//input[@type='submit']");
if (submit != null) {
return submit.click();
}
form.submit((SubmittableElement) this);
webClient.getJavaScriptEngine().processPostponedActions();
}
return webClient.getCurrentWindow().getEnclosedPage();
}
/**
* Simulates typing the specified key code while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc.
* Note also that for some elements, typing XXXXXXXXXXX
* submits the enclosed form.
*
* An example of predefined values is {@link KeyboardEvent#DOM_VK_PAGE_DOWN}.
*
* @param keyCode the key code to simulate typing
* @return the page that occupies this window after typing
*/
public Page type(final int keyCode) {
return type(keyCode, true, true, true, true);
}
/**
* Simulates typing the specified {@link Keyboard} while this element has focus, returning the page contained
* by this element's window after typing. Note that it may or may not be the same as the original page,
* depending on the JavaScript event handlers, etc.
* Note also that for some elements, typing XXXXXXXXXXX
* submits the enclosed form.
*
* @param keyboard the keyboard
* @return the page that occupies this window after typing
* @exception IOException if an IO error occurs
*/
public Page type(final Keyboard keyboard) throws IOException {
Page page = null;
final List