15
DOM Extensions

WHAT'S IN THIS CHAPTER?

  • Understanding the Selectors API
  • Using HTML5 DOM extensions

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter's code download on the book's website at www.wrox.com/go/projavascript4e on the Download Code tab.

Even though the DOM is a fairly well-defined API, it is also frequently augmented with both standards-based and proprietary extensions to provide additional functionality. Prior to 2008, almost all of the DOM extensions found in browsers were proprietary. After that point, the W3C went to work to codify some of the proprietary extensions that had become de facto standards into formal specifications.

The two primary standards specifying DOM extensions are the Selectors API and HTML5. These both arose out of needs in the development community and a desire to standardize certain approaches and APIs. There is also a smaller Element Traversal specification with additional DOM properties. Proprietary extensions still exist, even though these two specifications, especially HTML5, cover a large number of DOM extensions. The proprietary extensions are also covered in this chapter.

All content in this chapter is supported by all major browsers meaning all vendor releases that have meaningful web traffic—unless otherwise stated.

SELECTORS API

One of the most popular capabilities of JavaScript libraries is the ability to retrieve a number of DOM elements matching a pattern specified using CSS selectors. Indeed, the library jQuery (www.jquery.com) is built completely around the CSS selector queries of a DOM document in order to retrieve references to elements instead of using getElementById() and getElementsByTagName().

The Selectors API (www.w3.org/TR/selectors-api) was started by the W3C to specify native support for CSS queries in browsers. All JavaScript libraries implementing this feature had to do so by writing a rudimentary CSS parser and then using existing DOM methods to navigate the document and identify matching nodes. Although library developers worked tirelessly to speed up the performance of such processing, there was only so much that could be done while the code ran in JavaScript. By making this a native API, the parsing and tree navigating can be done at the browser level in a compiled language and thus tremendously increase the performance of such functionality.

At the core of Selectors API Level 1 are two methods: querySelector() and querySelectorAll(). On a conforming browser, these methods are available on the Document type and on the Element type.

The Selectors API Level 2 specification (https://www.w3.org/TR/selectors-api2/) introduces more methods: matches(), find(), and findAll() on the Element type, although no browsers currently have or have stated an intention to support find(), or findAll().

The querySelector() Method

The querySelector() method accepts a CSS query and returns the first descendant element that matches the pattern or null if there is no matching element. Here is an example:

// Get the body element
let body = document.querySelector("body");
          
// Get the element with the ID "myDiv"
let myDiv = document.querySelector("#myDiv");
          
// Get first element with a class of "selected"
let selected = document.querySelector(".selected");
          
// Get first image with class of "button"
let img = document.body.querySelector("img.button");

When the querySelector() method is used on the Document type, it starts trying to match the pattern from the document element; when used on an Element type, the query attempts to make a match from the descendants of the element only.

The CSS query may be as complex or as simple as necessary. If there's a syntax error or an unsupported selector in the query, then querySelector() throws an error.

The querySelectorAll() Method

The querySelectorAll() method accepts the same single argument as querySelector()—the CSS query—but returns all matching nodes instead of just one. This method returns a static instance of NodeList.

To clarify, the return value is actually a NodeList with all of the expected properties and methods, but its underlying implementation acts as a snapshot of elements rather than a dynamic query that is constantly reexecuted against a document. This implementation eliminates most of the performance overhead associated with the use of NodeList objects.

Any call to querySelectorAll() with a valid CSS query will return a NodeList object regardless of the number of matching elements; if there are no matches, the NodeList is empty.

As with querySelector(), the querySelectorAll() method is available on the Document, DocumentFragment, and Element types. Here are some examples:

// Get all <em> elements in a <div> (similar to getElementsByTagName("em"))
let ems = document.getElementById("myDiv").querySelectorAll("em");
          
// Get all elements wthat have "selected" as a class
let selecteds = document.querySelectorAll(".selected");
          
// Get all <strong> elements inside of <p> elements
let strongs = document.querySelectorAll("p strong");

The resulting NodeList object may be iterated over using iteration hooks, item(), or bracket notation to retrieve individual elements. Here's an example:

let strongElements = document.querySelectorAll("p strong");

// All three of the following loops will have the same effect:

for (let strong of strongElements) {
 strong.className = "important";
}

for (let i = 0; i < strongElements.length; ++i) {
 strongElements.item(i).className = "important";
}

for (let i = 0; i < strongElements.length; ++i) {
 strongElements [i].className = "important";
}

As with querySelector(), querySelectorAll() throws an error when the CSS selector is not supported by the browser or if there's a syntax error in the selector.

The matches() Method

matches() was formerly referred to as matchesSelector() in the specification draft. This method accepts a single argument, a CSS selector, and returns true if the given element matches the selector or false if not. For example:

if (document.body.matches ("body.page1")){
 // true
}

This method allows you to easily check if an element would be returned by querySelector() or querySelectorAll() when you already have the element reference.

All major browsers support some form of matches(). Edge, Chrome, Firefox, Safari, and Opera fully support it; IE 9–11 and minor mobile browsers support it with vendor prefixes.

ELEMENT TRAVERSAL

Prior to version 9, Internet Explorer did not return text nodes for white space in between elements while all of the other browsers did. This led to differences in behavior when using properties such as childNodes and firstChild. In an effort to equalize the differences while still remaining true to the DOM specification, a new group of properties was defined in the Element Traversal (www.w3.org/TR/ElementTraversal/).

The Element Traversal API adds five new properties to DOM elements:

  1. childElementCount—Returns the number of child elements (excludes text nodes and comments).
  2. firstElementChild—Points to the first child that is an element. Element-only version offirstChild.
  3. lastElementChild—Points to the last child that is an element. Element-only version oflastChild.
  4. previousElementSibling—Points to the previous sibling that is an element. Element-only version ofpreviousSibling.
  5. nextElementSibling—Points to the next sibling that is an element. Element-only version of nextSibling.

Supporting browsers add these properties to all DOM elements to allow for easier traversal of DOM elements without the need to worry about white space text nodes.

For example, iterating over all child elements of a particular element in a traditional cross-browser way looks like this:

let parentElement = document.getElementById('parent');
let currentChildNode = parentElement.firstChild;

// For zero children, firstChild returns null and the loop is skipped
while (currentChildNode) {
 if (currentChildNode.nodeType === 1) {
   // If this is an ELEMENT_NODE, do whatever work is needed in here
   processChild(currentChildNode);
 }
  if (currentChildNode === parentElement.lastChild) {
    break;
 }
 currentChildNode = currentChildNode.nextSibling;
}

Using the Element Traversal properties allows a simplification of the code:

let parentElement = document.getElementById('parent');
let currentChildElement = parentElement.firstElementChild;

// For zero children, firstElementChild returns null and the loop is skipped
while (currentChildElement) {
 // You already know this is an ELEMENT_NODE, do whatever work is needed here
 processChild(currentChildElement);
 if (currentChildElement === parentElement.lastElementChild) {
   break;
 }
 currentChildElement = currentChildElement.nextElementSibling;
}

Element Traversal is implemented in Internet Explorer 9+ and all modern browsers.

HTML5

HTML5 represents a radical departure from the tradition of HTML. In all previous HTML specifications, the descriptions stopped short of describing any JavaScript interfaces, instead focusing purely on the markup of the language and deferring JavaScript bindings to the DOM specification.

The HTML5 specification, on the other hand, contains a large amount of JavaScript APIs designed for use with the markup additions. Part of these APIs overlap with the DOM and define DOM extensions that browsers should provide.

Class-Related Additions

One of the major changes in web development since the time HTML4 was adopted is the increased usage of the class attribute to indicate both stylistic and semantic information about elements. This caused a lot of JavaScript interaction with CSS classes, including the dynamic changing of classes and querying the document to find elements with a given class or set of classes. To adapt to developers and their newfound appreciation of the class attribute, HTML5 introduces a number of changes to make CSS class usage easier.

The getElementsByClassName() Method

One of HTML5's most popular additions is getElementsByClassName(), which is available on the document object and on all HTML elements. This method evolved out of JavaScript libraries that implemented it using existing DOM features and is provided as a native implementation for performance reasons.

The getElementsByClassName() method accepts a single argument, which is a string containing one or more class names, and returns a NodeList containing all elements that have all of the specified classes applied. If multiple class names are specified, then the order is considered unimportant. Here are some examples:

// Get all elements with a class containing "username" and "current"
// It does not matter if one is declared before the other
let allCurrentUsernames = document.getElementsByClassName("username current");
          
// Get all elements with a class of "selected" that exist in myDiv's subtree
let selected = document.getElementById("myDiv").getElementsByClassName("selected");

When this method is called, it will return only elements in the subtree of the root from which it was called. Calling getElementsByClassName() on document always returns all elements with matching class names, whereas calling it on an element will return only descendant elements.

This method is useful for attaching events to classes of elements rather than using IDs or tag names. Keep in mind that since the returned value is a NodeList, there are the same performance issues as when you're using getElementsByTagName() and other DOM methods that return NodeList objects.

The getElementsByClassName() method is implemented in Internet Explorer 9+ and all modern browsers.

The classList Property

In class name manipulation, the className property is used to add, remove, and replace class names. Because className contains a single string, it's necessary to set its value every time a change needs to take place, even if there are parts of the string that should be unaffected. For example, consider the following HTML code:

<div class="bd user disabled">…</div>

This <div> element has three classes assigned. To remove one of these classes, you need to split the class attribute into individual classes, remove the unwanted class, and then create a string containing the remaining classes. Here is an example:

// Remove the "user" class
let targetClass = "user";
          
// First, get list of class names
let classNames = div.className.split(/s+/);
          
// Find the class name to remove
let idx = classNames.indexOf(targetClass);
          
// Remove the class name if found
if (idx> -1) { 
 classNames.splice(i,1);
}
          
// Set back the class name
div.className = classNames.join(" ");

All of this code is necessary to remove the "user" class from the <div> element's class attribute. A similar algorithm must be used for replacing class names and detecting if a class name is applied to an element. Adding class names can be done by using string concatenation, but checks must be done to ensure that you're not applying the same class more than one time. Many JavaScript libraries implement methods to aid in these behaviors.

HTML5 introduces a way to manipulate class names in a much simpler and safer manner through the addition of the classList property for all elements. The classList property is an instance of a new type of collection named DOMTokenList. As with other DOM collections, DOMTokenList has a length property to indicate how many items it contains, and individual items may be retrieved via the item() method or using bracket notation. It also has the following additional methods:

  1. add(value )—Adds the given string value to the list. If the value already exists, it will not be added.
  2. contains(value )—Indicates if the given value exists in the list (true if so; false if not).
  3. remove(value )—Removes the given string value from the list.
  4. toggle(value)—If the value already exists in the list, it is removed. If the value doesn't exist, then it's added.

The entire block of code in the previous example can quite simply be replaced with the following:

div.classList.remove("user");

Using this code ensures that the rest of the class names will be unaffected by the change. The other methods also greatly reduce the complexity of the basic operations, as shown in these examples:

// Remove the "disabled" class
div.classList.remove("disabled");
          
// Add the "current" class
div.classList.add("current");
          
// Toggle the "user" class
div.classList.toggle("user");
          
// Figure out what's on the element now
if (div.classList.contains("bd") && !div.classList.contains("disabled")){
 // Do stuff 
)
          
// Iterate over the class names
for (let class of div.classList){
 doStuff(class);
}

The addition of the classList property makes it unnecessary to access the className property unless you intend to completely remove or completely overwrite the element's class attribute. The classList property is implemented partially in Internet Explorer 10+ and fully in all other major browsers.

Focus Management

HTML5 adds functionality to aid with focus management in the DOM. The first is document.activeElement, which always contains a pointer to the DOM element that currently has focus. An element can receive focus automatically as the page is loading, via user input (typically using the Tab key), or programmatically using the focus() method. For example:

let button = document.getElementById("myButton");
button.focus();
console.log(document.activeElement === button); // true

By default, document.activeElement is set to document.body when the document is first loaded. Before the document is fully loaded, document.activeElement is null.

The second addition is document.hasFocus(), which returns a Boolean value indicating if the document has focus:

let button = document.getElementById("myButton");
button.focus();
console.log(document.hasFocus()); // true

Determining if the document has focus allows you to determine if the user is interacting with the page.

This combination of being able to query the document to determine which element has focus and being able to ask the document if it has focus is of the utmost importance for web application accessibility. One of the key components of accessible web applications is proper focus management, and being able to determine which elements currently have focus is a major improvement over the guesswork of the past.

Changes to HTMLDocument

HTML5 extends the HTMLDocument type to include more functionality. As with other DOM extensions specified in HTML5, the changes are based on proprietary extensions that are well-supported across browsers. As such, even though the standardization of the extensions is relatively new, some browsers have supported the functionality for a while.

The readyState Property

Internet Explorer 4 was the first to introduce a readyState property on the document object, and it is supported by all modern browsers. Other browsers then followed suit and this property was eventually formalized in HTML5. The readyState property for document has two possible values:

  1. loading—The document is loading.
  2. complete—The document is completely loaded.

The best way to use the document.readyState property is as an indicator that the document has loaded. Before this property was widely available, you would need to add an onload event handler to set a flag indicating that the document was loaded. Basic usage:

if (document.readyState == "complete"){
 // Do stuff
}

Compatibility Mode

With the introduction of Internet Explorer 6 and the ability to render a document in either standards or quirks mode, it became necessary to determine in which mode the browser was rendering the page. Internet Explorer added a property on the document named compatMode whose sole job is to indicate what rendering mode the browser is in. As shown in the following example, when in standards mode, document.compatMode is equal to "CSS1Compat"; when in quirks mode, document.compatMode is "BackCompat":

if (document.compatMode == "CSS1Compat"){
  console.log("Standards mode");
} else {
  console.log("Quirks mode");
}

The compatMode property was added to HTML5 to formalize its implementation.

The head Property

HTML5 introduces document.head to point to the <head> element of a document to complement document.body, which points to the <body> element of the document. You can retrieve a reference to the <head> element using this property:

let head = document.head;

Character Set Properties

HTML5 describes several new properties dealing with the character set of the document. The characterSet property indicates the actual character set being used by the document and can also be used to specify a new character set. By default, this value is "UTF-16", although it may be changed by using <meta> elements or response headers or through setting the characterSet property directly. Here's an example:

console.log(document.characterSet);  // "UTF-16"
document.characterSet = "UTF-8";

Custom Data Attributes

HTML5 allows elements to be specified with nonstandard attributes prefixed with data- in order to provide information that isn't necessary to the rendering or semantic value of the element. These attributes can be added as desired and named anything, provided that the name begins with data-. Here is an example:

<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>

When a custom data attribute is defined, it can be accessed via the dataset property of the element. The dataset property contains an instance of DOMStringMap that is a mapping of name-value pairs. Each attribute of the format data-name is represented by a property with a name equivalent to the attribute but without the data- prefix (for example, attribute data-myname is represented by a property called myname). The following is an example of how to use custom data attributes:

// Methods used in this example are for illustrative purposes only
          
let div = document.getElementById("myDiv");
          
// Get the values
let appId = div.dataset.appId;
let myName = div.dataset.myname;
          
// Set the value
div.dataset.appId = 23456;
div.dataset.myname = "Michael";
          
// Is there a "myname" value?
if (div.dataset.myname){
 console.log('Hello, ${div.dataset.myname}');
}

Custom data attributes are useful when nonvisual data needs to be tied to an element for some other form of processing. This is a common technique to use for link tracking and mashups in order to better identify parts of a page. It is also extensively utilized in numerous single-page application frameworks.

Markup Insertion

Although the DOM provides fine-grained control over nodes in a document, it can be cumbersome when attempting to inject a large amount of new HTML into the document. Instead of creating a series of DOM nodes and connecting them in the correct order, it's much easier (and faster) to use one of the markup insertion capabilities to inject a string of HTML. The following DOM extensions have been standardized in HTML5 for this purpose.

The innerHTML Property

When used in read mode, innerHTML returns the HTML representing all of the child nodes, including elements, comments, and text nodes. When used in write mode, innerHTML completely replaces all of the child nodes in the element with a new DOM subtree based on the specified value. Consider the following HTML code:

<div id="content">
 <p>This is a <strong>paragraph</strong> with a list following it.</p>
 <ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
 </ul>
</div>

For the <div> element in this example, the innerHTML property returns the following string:

<p>This is a <strong>paragraph</strong> with a list following it.</p>
<ul>
 <li>Item 1</li>
 <li>Item 2</li>
 <li>Item 3</li>
</ul>

The exact text returned from innerHTML differs from browser to browser. Internet Explorer and Opera tend to convert all tags to uppercase, whereas Safari, Chrome, and Firefox return HTML in the way it is specified in the document, including white space and indentation. You cannot depend on the returned value of innerHTML being exactly the same from browser to browser.

When used in write mode, innerHTML parses the given string into a DOM subtree and replaces all of the existing child nodes with it. Because the string is considered to be HTML, all tags are converted into elements in the standard way that the browser handles HTML (again, this differs from browser to browser). Setting simple text without any HTML tags, as shown here, sets the plain text:

div.innerHTML = "Hello world!"; 

Setting innerHTML to a string containing HTML behaves quite differently as innerHTML parses them. Consider the following example:

div.innerHTML = "Hello & welcome, <b>"reader"!</b>";

The result of this operation is as follows:

<div id="content">Hello & welcome, <b>"reader"!</b></div>

After setting innerHTML, you can access the newly created nodes as you would any other nodes in the document.

Using innerHTML in Legacy Internet Explorer

<script> elements cannot be executed when inserted via innerHTML in all modern browsers. Internet Explorer 8 and earlier, is the only browser that allows this but only as long as the defer attribute is specified and the <script> element is preceded by what Microsoft calls a scoped element. The <script> element is considered a NoScope element, which effectively means that it has no visual representation on the page, like a <style> element or a comment. Internet Explorer strips out all NoScope elements from the beginning of strings inserted via innerHTML, which means the following won't work:

// Won't work
div.innerHTML = "<script defer>console.log('hi');</script>";

In this case, the innerHTML string begins with a NoScope element, so the entire string becomes empty. To allow this script to work appropriately, you must precede it with a scoped element, such as a text node or an element without a closing tag such as <input>. The following lines will all work:

// All these will work
div.innerHTML = "_<script defer>console.log('hi');</script>";
div.innerHTML = "<div> </div><script defer>console.log('hi');</script>";
div.innerHTML = "<input type="hidden"><script defer>console.log('hi');</script>";

The first line results in a text node being inserted immediately before the <script> element. You may need to remove this after the fact so as not to disrupt the flow of the page. The second line has a similar approach, using a <div> element with a nonbreaking space. An empty <div> alone won't do the trick; it must contain some content that will force a text node to be created. Once again, the first node may need to be removed to avoid layout issues. The third line uses a hidden <input> field to accomplish the same thing. Because it doesn't affect the layout of the page, this may be the optimal case for most situations.

In most browsers, the <style> element causes similar problems with innerHTML. Most browsers support the insertion of <style> elements using innerHTML in the exact way you'd expect, as shown here:

div.innerHTML = "<style type="text/css">body {background-color: red; }</style>";

In Internet Explorer 8 and earlier, <style> is yet another NoScope element, so it must be preceded by a scoped element such as this:

div.innerHTML = "_<style type="text/css">body {background-color: red; }</style>";
div.removeChild(div.firstChild);

The outerHTML Property

When outerHTML is called in read mode, it returns the HTML of the element on which it is called, as well as its child nodes. When called in write mode, outerHTML replaces the node on which it is called with the DOM subtree created from parsing the given HTML string. Consider the following HTML code:

<div id="content">
 <p>This is a <strong>paragraph</strong> with a list following it.</p>
 <ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
 </ul>
</div>

When outerHTML is called on the <div> in this example, the same code is returned, including the code for the <div>. Note that there may be differences based on how the browser parses and interprets the HTML code. (These are the same types of differences you'll notice when using innerHTML.)

Use outerHTML to set a value in the following manner:

div.outerHTML = "<p>This is a paragraph.</p>";

This code performs the same operation as the following DOM code:

let p = document.createElement("p");
p.appendChild(document.createTextNode("This is a paragraph."));
div.parentNode.replaceChild(p, div);

The new <p> element replaces the original <div> element in the DOM tree.

The insertAdjacentHTML() and insertAdjacentText() Methods

The last addition for markup insertion is the insertAdjacentHTML() and insertAdjacentText() methods. These methods also originated in Internet Explorer and accept two arguments: the position in which to insert and the HTML or text to insert. The first argument must be one of the following values:

  1. "beforebegin"—Insert just before the element as a previous sibling.
  2. "afterbegin"—Insert just inside of the element as a new child or series of children before the first child.
  3. "beforeend"—Insert just inside of the element as a new child or series of children after the last child.
  4. "afterend"—Insert just after the element as a next sibling.

Note that each of these values is case insensitive. The second argument is parsed as an HTML string (the same as with innerHTML/outerHTML) or as a raw string (the same as with innerText/outerText) and in the case of HTML, will throw an error if the value cannot be properly parsed. Basic usage is as follows:

// Insert as previous sibling
element.insertAdjacentHTML("beforebegin", "<p>Hello world!</p>");
element.insertAdjacentText("beforebegin", "Hello world!");

// Insert as first child
element.insertAdjacentHTML("afterbegin", "<p>Hello world!</p>");
element.insertAdjacentText("afterbegin", "Hello world!");

// Insert as last child
element.insertAdjacentHTML("beforeend", "<p>Hello world!</p>");
element.insertAdjacentText("beforeend", "Hello world!");

// Insert as next sibling
element.insertAdjacentHTML("afterend", "<p>Hello world!</p>"); element.insertAdjacentText("afterend", "Hello world!");

Memory and Performance Issues

Replacing child nodes using the methods in this section may cause memory problems in browsers, especially Internet Explorer. The problem occurs when event handlers or other JavaScript objects are assigned to subtree elements that are removed. If an element has an event handler (or a JavaScript object as a property), and one of these properties is used in such a way that the element is removed from the document tree, the binding between the element and the event handler remains in memory. If this is repeated frequently, memory usage increases for the page. When using innerHTML, outerHTML, and insertAdjacentHTML(), it's best to manually remove all event handlers and JavaScript object properties on elements that are going to be removed.

Using these properties does have an upside, especially when using innerHTML. Generally speaking, inserting a large amount of new HTML is more efficient through innerHTML than through multiple DOM operations to create nodes and assign relationships between them. This is because an HTML parser is created whenever a value is set to innerHTML (or outerHTML). This parser runs in browser-level code (often written in C++), which is must faster than JavaScript. That being said, the creation and destruction of the HTML parser does have some overhead, so it's best to limit the number of times you set innerHTML or outerHTML. For example, the following creates a number of list items using innerHTML:

for (let value of values){
 ul.innerHTML += '<li>${value}</li>'; // avoid!!
} 

This code is inefficient because it sets innerHTML once each time through the loop. Furthermore, this code is reading innerHTML each time through the loop, meaning that innerHTML is being accessed twice each time through the loop. It's best to build up the string separately and assign it using innerHTML just once at the end, like this:

let itemsHtml = "";
for (let value of values){
 itemsHtml += '<li>${value}</li>';
} 
ul.innerHTML = itemsHtml;

This example is more efficient, limiting the use of innerHTML to one assignment. Of course, if you wanted to condense it to a single line:

ul.innerHTML = values.map(value => '<li>${value}</li>').join('');

Cross-Site Scripting Considerations

Although innerHTML does not execute script tags that it creates, it still provides an extremely broad attack surface for malicious actors looking to compromise a web page because it so readily creates elements and executable attributes such as onclick.

Anywhere you are interpolating user-provided information into the page, it is nearly always inadvisable to do so using innerHTML. The headaches of preventing XSS vulnerabilities far outweigh any convenience benefits gained from using innerHTML. Compartmentalize interpolated data, and don't hesitate to use libraries that escape interpolated data before inserting them into the page.

The scrollIntoView() Method

One of the issues not addressed by the DOM specification is how to scroll areas of a page. To fill this gap, browsers implemented several methods that control scrolling in different ways. Of the various proprietary methods, only scrollIntoView() was selected for inclusion in HTML5.

The scrollIntoView() method exists on all HTML elements and scrolls the browser window or container element so the element is visible in the viewport.

  1. If an argument of true is supplied, it specifies alignToTop: The window scrolls so that the top of the element is at the top of the viewport.
  2. If an argument of false is supplied, it specifies alignToTop: The window scrolls so that the bottom of the element is at the top of the viewport.
  3. If an object argument is supplied, the user can provide values for the behavior property, which specifies how the scroll should occur: auto, instant, or smooth (limited support outside of Firefox), and the block property is the same as alignToTop.
  4. If no argument is supplied, the element is scrolled so that it is fully visible in the viewport but may not be aligned at the top. For example:
    // Ensures this element is visible
    document.forms[0].scrollIntoView();
    
    // These behave identically
    document.forms[0].scrollIntoView(true); 
    document.forms[0].scrollIntoView({block: true});
    
    // This attempts to scroll the element smoothly into view:
    document.forms[0].scrollIntoView({behavior: 'smooth', block: true});

This method is most useful for getting the user's attention when something has happened on the page. Note that setting focus to an element also causes the browser to scroll the element into view so that the focus can properly be displayed.

PROPRIETARY EXTENSIONS

Although all browser vendors understand the importance of adherence to standards, they all have a history of adding proprietary extensions to the DOM in order to fill perceived gaps in functionality. Though this may seem like a bad thing on the surface, proprietary extensions have given the web development community many important features that were later codified into standards such as HTML5.

There are still a large amount of DOM extensions that are proprietary in nature and haven't been incorporated into standards. This doesn't mean that they won't later be adopted as standards—just that at the time of this writing, they remain proprietary and adopted by only a subset of browsers.

The children Property

The differences in how Internet Explorer prior to version 9 and other browsers interpret white space in text nodes led to the creation of the children property. The children property is an HTMLCollection that contains only an element's child nodes that are also elements. Otherwise, the children property is the same as childNodes and may contain the same items when an element has only elements as children. The children property is accessed as follows:

let childCount = element.children.length;
let firstChild = element.children[0];

The contains() Method

It's often necessary to determine if a given node is a descendant of another. Internet Explorer first introduced the contains() method as a way of providing this information without necessitating a walk up the DOM document tree. The contains() method is called on the ancestor node from which the search should begin and accepts a single argument, which is the suspected descendant node. If the node exists as a descendant of the root node, the method returns true; otherwise it returns false. Here is an example:

console.log(document.documentElement.contains(document.body)); // true

This example tests to see if the <body> element is a descendant of the <html> element, which returns true in all well-formed HTML pages.

There is another way of determining node relationships by using the DOM Level 3 compareDocumentPosition() method. This method determines the relationship between two nodes and returns a bitmask indicating the relationship. The values for the bitmask are as shown in the following table.

MASK RELATIONSHIP BETWEEN NODES
0x1 Disconnected (The passed-in node is not in the document.)
0x2 Precedes (The passed-in node appears in the DOM tree prior to the reference node.)
0x4 Follows (The passed-in node appears in the DOM tree after the reference node.)
0x8 Contains (The passed-in node is an ancestor of the reference node.)
0x10 Is contained by (The passed-in node is a descendant of the reference node.)

To mimic the contains() method, you will be interested in the 16 mask. The result of compareDocumentPosition() can be bitwise ANDed to determine if the reference node contains the given node. Here is an example:

let result = document.documentElement.compareDocumentPosition(document.body);
console.log(!!(result & 0x10));

When this code is executed, result becomes 20, or 0x14 (0x4 for “follows” plus 0x10 for “is contained by”). Applying a bitwise mask of 0x10 to the result returns a nonzero number, and the two NOT bang operators convert that value into a Boolean.

IE9+ and all modern browsers support both contains and compareDocumentPosition.

Markup Insertion

While the innerHTML and outerHTML markup insertion properties were adopted by HTML5 from Internet Explorer, there are two others that were not. The two remaining properties that are left out of HTML5 are innerText and outerText.

The innerText Property

The innerText property works with all text content contained within an element, regardless of how deep in the subtree the text exists. When used to read the value, innerText concatenates the values of all text nodes in the subtree in depth-first order. When used to write the value, innerText removes all children of the element and inserts a text node containing the given value. Consider the following HTML code:

<div id="content">
 <p>This is a <strong>paragraph</strong> with a list following it.</p>
 <ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
 </ul>
</div>

For the <div> element in this example, the innerText property returns the following string:

This is a paragraph with a list following it.
Item 1
Item 2
Item 3

Note that different browsers treat white space in different ways, so the formatting may or may not include the indentation in the original HTML code.

Using the innerText property to set the contents of the <div> element is as simple as this single line of code:

div.innerText = "Hello world!"; 

After executing this line of code, the HTML of the page is effectively changed to the following:

<div id="content">Hello world!</div>

Setting innerText removes all of the child nodes that existed before, completely changing the DOM subtree. Additionally, setting innerText encodes all HTML syntax characters (less-than, greater-than, quotation marks, and ampersands) that may appear in the text. Here is an example:

div.innerText = "Hello & welcome, <b>"reader"!</b>";

The result of this operation is as follows:

<div id="content">Hello & welcome, <b>"reader"!</b></div>

Setting innerText can never result in anything other than a single text node as the child of the container, so the HTML-encoding of the text must take place in order to keep to that single text node. The innerText property is also useful for stripping out HTML tags. By setting the innerText equal to the innerText, as shown here, all HTML tags are removed:

div.innerText = div.innerText;

Executing this code replaces the contents of the container with just the text that exists already.

The outerText Property

The outerText property works in the same way as innerText except that it includes the node on which it's called. For reading text values, outerText and innerText essentially behave in the exact same way. In writing mode, however, outerText behaves very differently. Instead of replacing just the child nodes of the element on which it's used, outerText actually replaces the entire element, including its child nodes. Consider the following:

div.outerText = "Hello world!";

This single line of code is equivalent to the following two lines:

let text = document.createTextNode("Hello world!");
div.parentNode.replaceChild(text, div);

Essentially, the new text node completely replaces the element on which outerText was set. After that point in time, the element is no longer in the document and cannot be accessed.

The outerText property is nonstandard, and not on a standards track. It is not recommended that you rely on it for important behavior. It is supported in all modern browsers except for Firefox.

Scrolling

As mentioned previously, scrolling is one area where specifications didn't exist prior to HTML5. While scrollIntoView() was standardized in HTML5, there are still several additional proprietary methods available in various browsers. scrollIntoViewIfNeeded exists as an extension to the HTMLElement type and therefore each is available on all elements. scrollIntoViewIfNeeded(alignCenter) scrolls the browser window or container element so that the element is visible in the viewport only if it's not already visible; if the element is already visible in the viewport, this method does nothing. The optional alignCenter argument will attempt to place the element in the center of the viewport if set to true. This is implemented in Safari, Chrome, and Opera.

Following is an example of how this may be used:

// Make sure this element is visible only if it's not already
document.images[0].scrollIntoViewIfNeeded();

Because scrollIntoView() is the only method supported in all browsers, this is typically the only one used.

SUMMARY

While the DOM specifies the core API for interacting with XML and HTML documents, there are several specifications that provide extensions to the standard DOM. Many of the extensions are based on proprietary extensions that later became de facto standards as other browsers began to mimic their functionality. The three specifications covered in this chapter are:

  1. Selectors API, which defines two methods for retrieving DOM elements based on CSS selectors: querySelector(), querySelectorAll(), and matches().
  2. Element Traversal, which defines additional properties on DOM elements to allow easy traversal to the next related DOM element. The need for this arose because of the handling of white space in the DOM that creates text nodes between elements.
  3. HTML5, which provides a large number of extensions to the standard DOM. These include standardization of de facto standards such as innerHTML, as well as additional functionality for dealing with focus management, character sets, scrolling, and more.

The number of DOM extensions is currently small, but it's almost a certainty that the number will continue to grow as web technology continues to evolve. Browsers still experiment with proprietary extensions that, if successful, may end up as pseudo-standards or be incorporated into future versions' specifications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset