Position/Dimension properties in Javascript

Position/Dimension properties in Javascript

Fundamental concepts

  • In general, x/ left suffices refer to the horizontal distance, while y/ top suffices refer to the vertical one.
  • View port is the area/portion of the web page (precisely speaking, the nearest iframe) that we can see  on the screen. Imagine a web page is an infinite landscape, your web browser is a camera and the area which is seen through the camera viewfinder is the view port. When you move the camera/viewfinder, the scene changes respectively.
  • Floating point values can be returned by or assigned to some properties, especially when the browser is zoomed or when elements are transformed. We will see in this post that scrollTop, scrollLeft, DOMRect are able to handle floating point, while the others are rounded to integer numbers.
  • getBoundingClientRect() and getClientRects() return the rendered dimension which takes care of transforms while the others APIs return the layout dimension and ignore all transforms.
  • The order of an element's displaying components from inner to outer are as follows: content → padding → scroll → border → margin. Verbally speaking, the area from content to border is considered belong to the element (is involved in the results returned from getBoundingClientRect or in offset* properties). Of which, only content and padding belong to the client area of an element (involved in client* properties).
  • The vertical scroll bar does not always stay on the right side of an element. For example, in a right-to-left mode, it is rendered in the left side.
  • While this is not related to any javascript API (except the window.getComputedStyle()), it is worth noting. box-sizing CSS property defines whether the CSS width/height includes padding, border (and thus, scroll bar).
    In the CSS standard, by default, box-sizing is content-box, implying that the CSS width/height properties define only the dimension of the content.
    While in practice, many frameworks, modern CSS reset library assign border-box to box-sizing, which means padding, border, scrollbar are included in the CSS width/height.
    In bootstrap's _reboot.scss:
// Document
//
// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.

*,
*::before,
*::after {
  box-sizing: border-box;
}
  • When an element becomes a scroll view (usually due to the larger size of its children), its content is the viewable area, not the larger being scrolled area which is the content of its children.

DOM node hierarchy inheritance structure

The most generic class is the node class, its unique super class is EventTarget. The are many types of node. The nodeType property is an integer used to define the type of a node, hence, the sub-class which it belongs to. List of node types sorted by their popularity are:

  • Node.ELEMENT_NODE: infers the sub-class Element.

Element is the most common node type with its 2 common descendant classes are HTMLElement ( <a>, <p>, <iframe> ... elements), and SVGElement ( <svg>, <path>, <g> ... elements).

  • Node.TEXT_NODE: infers the sub-class Text. For example: <span>hello world</span> is a Element node with a single Text node whose wholeText property is 'hello world'.
  • Node.COMMENT_NODE: infers the sub-class Comment, represents comment nodes <!-- ... -->.
  • Node.DOCUMENT_NODE: infers the sub-class Document where the document variable instantiated from.
  • Node.DOCUMENT_TYPE_NODE: infers the sub-class DocumentType, represents <!DOCTYPE html> node.
  • Node.CDATA_SECTION_NODE: infers the sub-class CDATASection, represents <!CDATA[[ ... ]]> nodes.
  • Other node types are less common: Node.ATTRIBUTE_NODE (Attribute sub-class), Node.PROCESSING_INSTRUCTION_NODE (ProcessingInstruction sub-class), Node.DOCUMENT_FRAGMENT_NODE (DocumentFragment sub-class).
  • And some types are deprecated: Node.ENTITY_REFERENCE_NODE, Node.ENTITY_NODE, Node.NOTATION_NODE.
window instanceof Window // true
window instanceof Node // false

document instanceof Node // true
document instanceof Document // true
document instanceof Element // false

document.documentElement instanceof Node // true
document.documentElement instanceof Element // true
document.documentElement instanceof HTMLElement // true

document.getElementsByTagName('path')[0] instanceof HTMLElement // false
document.getElementsByTagName('path')[0] instanceof SVGElement // true

Suffix naming convention

Common (not always true, for example mouse event's client* are exception), suffices used in properties are:

  • screen*: refer to the position in the whole Operating System. If there are multiple monitors, the monitor in left and top are also involved.
  • page*: related to the web page root element, document.documentElement.
  • scroll*: when an element turns in to a scroll view, this refers to the whole area which require the scroll.
  • offset*: element's rendered area (from inner up to border).
  • client*: element's area used to display its children (from inner up to padding).

Root element is a special case

The root element (the <html> element in quirks mode, or, the <body> element when not in quirks mode) is an instance of HTMLElement (thus, Element). It has the same properties with all other HTML elements, but some of the properties have particularly different definitions by the specs. By convention, through this post, I will use the term root-element, and it should be considered as an <html> or <body> element depending on whether the document is in quirks mode.

The following codepen explains why the root-element should be treated differently. border, padding styling are normally applicable but overflow has no effect. The children always overflow the root element regardless of the overflow styling. The height styling determines the position of the border but the background is applied on the whole web page instead of the area within the border.

To rephrase it, the scroll bar will never appear on the root element. The specs takes this advantage to control the browser's scroll bar (window.scroll*) when the APIs are called on the root document (document.documentElement.{scroll*,client*}).

!!Important note!!: the browser's scroll bar does NOT belong to the root element, check the codepen, when the scroll bar appears, it stays outside of the root element's border.

Mouse event

Mouse Event's properties, in a mouse related events, such as mousedown, mouseenter, mouseleave, mousemove, mouseout, mouseover.

  • clientX, clientY.
    Mouse position relative to the view port  (of the nearest iframe). Try running the code pen. Note that the code pen itself is an embedded iframe.

    Compatibility is unknown in Firefox, IE, Safari.
  • movementX, movementY.
    Mouse position relative to the previous mouse event.

    Not supported in IE.
  • pageX, pageY.
    Mouse position relative to the whole web page content (of the nearest iframe).

    Not supported in Firefox. Compatibility is unknown in IE, Safari.
  • screenX, screenY.
    Mouse position relative to your operating system's displaying screen. Note that if you have multiple monitors, left and top monitors's size are added to these values.

    Compatibility is unknown in Firefox, IE, Safari.

These values are all floating point numbers.

Codepen

Compatibility

While all properties' compatibility is unknown in Firefox, Safari, the unknown compatibility is likely considered as supported rather than unsupported. For pageX, pageY which are not supported in Firefox, you can use this polyfill from jquery.

			// Calculate pageX/Y if missing and clientX/Y available
			if ( event.pageX == null && original.clientX != null ) {
				eventDoc = event.target.ownerDocument || document;
				doc = eventDoc.documentElement;
				body = eventDoc.body;

				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
			}

None-standard properties

Element

The codepen for this section.

getClientRects() typically returns an array of single DOMRect object. Except, for <textarea>, <svg>, display: none elements or elements that are not directly rendered, the returned list is empty.
For <span> elements rendered in multiple lines, multiple DOMRect objects are included in the returned array.

In general, getBoundingClientRect is adequate for general uses.

These methods take care of transforms, while other properties ( client*, scroll*, offset*, screen*, inner*, outer*, ...) return the layout dimension which does not involve transforms. From MDN Web Docs:

In case of transforms, the offsetWidth and offsetHeight returns the element's layout width and height, while getBoundingClientRect() returns the rendering width and height. As an example, if the element has width: 100px; and transform: scale(0.5); the getBoundingClientRect() will return 50 as the width, while offsetWidth will return 100.

Both these 2 APIs have a high compatibility and are very safe  to be used. However, the returned DOMRect objects requires more careful treatment. The specs and caveats for the DOMRect object is worth a separate sub-section latter in this section.

When the element is the root-element, and the browser's scroll bar appears, getBoundingClientRect() excludes the scroll bar area because the scroll bar does not belong to the root element. There is not a special definition here.

Similar to clientWidth / clientHeight, in the special case, window becomes the scrolling element itself. Note that although having the same names, these methods are different from window.scroll(), window.scrollBy(), window.scrollTo(). In IE, all these methods are not available (this is not true for window's methods).

Safari supports all these APIs but does not support the scroll ScrollToOptions option, which specifies the behavior option  for smooth scroll. However, a polyfill is available. If you are using ES6 dynamic import:

if (!('scrollBehavior' in document.documentElement.style)) {
//safari does not support smooth scroll
  (async () => {
    const {default: smoothScroll} = await import(
      /* webpackChunkName: 'polyfill-modern' */
      'smoothscroll-polyfill'
      )
    smoothScroll.polyfill()
  })()
}

This API is supported by all major browsers. However, IE and Safari does not support the scrollIntoViewOptions, which specifies behavior, block, inline, and the being proposed scroll (or scrollMode?, it is going to fixed) options.

There is a popular ponyfill (in short, a polyfill which does not hook into the native implementation and ignore the native implementation if exists, which eliminates the inconsistency across browsers due to bugs, etc.).  To support the if-needed scrollMode use the scroll-into-view-if-needed package (and optionally use the smooth behavior if the browser natively supports), to support both the smooth behavior and if-needed mode, use the smooth-scroll-into-view-if-needed. Note that the latter does not follow the specs and adds additional features by it own. Both these 2 packages use the popular package compute-scroll-into-view internally to positioning the element.

  • clientHeight, clientWidth.
    The values are rounded to integer numbers. They are read-only. If the element is inline or has no associated CSS layout box, these values become 0.

    They are equal to content size + padding size (excluded if hidden in a scroll view), the scroll bar size and outer components are not included.

    There is a special case of this definition, if the element is the root-element, these values are equal to the view port's size excluding scroll bar size (if present). The special treatment is logically acceptable because the root element when being larger than the view port, it requires a parent element (with a scroll bar) to scroll around, but it is already the root element, so it becomes the scroll view of itself.
Image from MDN Web Docs
  • clientLeft, clientTop.
    The values are rounded to integer numbers. They are read-only. If the element is inline or has no associated CSS layout box, these values become 0.

    These values have corresponding meanings with clientHeight/ clientWidth, which are the width of the top/left border. In the case of clientLeft, if the scroll bar appears in the left side, the scroll bar width is included.

    Because the root-element can have border, there is not any special definition for the root element. Because the root element can not obtain a scroll bar, these value are always equal to the border dimension.
  • scrollHeight, scrollWidth.
    The values are rounded to integer numbers. They are read-only.

    In conjunction with clientHeight/ clientWidth, these values represent the size of the area scrolled by element's scroll. That is to say, the dimension required to include all element's children, including the pseudo-elements such as ::before/ ::after. When the children fit into the element size, these values are equal to clientHeight / clientWidth, respectively.
Image from MDN Web Docs
  • scrollLeft, scrollTop.
    These values can be modified to move the scroll position. They can receive/keep decimal values.
    scrollLeft is non-negative in left-to-right setting, non-positive in right-to-left setting, while scrollTop is always non-negative.
    In value assignment, the values are clipped in the acceptable range (defined by the scrollable value) before the assignment.
    When there is no scroll bar, these values are 0.

    There is a special definition, if the element is the root-document, these values refer to window.scrollX / window.scrollY, respectively, in both value retrieval and assignment.

When the scroll reaches the end, these expressions will be evaluated to true (depend on the scrolling axis). They can be used to determine whether the user scrolls to the end of an element.

// horizontal scroll
elm.scrollWidth === Math.abs(elm.scrollLeft) + elm.clientWidth

// vertical scroll
elm.scrollHeight === elm.scrollTop + elm.clientHeight

The DOMRect object

DOMRect class inherits DOMRectReadOnly (IE, Edge). They have the same properties except that DOMRectReadOnly does not allow modification to the object.

The available properties in this object are left, top, right, bottom, x, y, width, height. They describe the rendered dimension/position of the element relative to top-left corner of the view port (not the document). For large elements that are covered by a scroll view, the whole element (including the hidden area) is considered. Content, padding, border (but not margin) are involved in the value of these properties.
To produce a high compatibility code, you should rely only on four properties left, top, right, bottom and calculate other properties manually if needed.

Because properties in DOMRect are not own properties. In other words, they does not appear in Object.keys(), and only appear in for ... in operator. So, the ES6 rest/spread operator or Object.assign() are not able to copy the properties.

None-standard properties

HTMLElement

HTMLElement inherits the Element class. Hence, it has all properties of Element, plus the following properties:

  • offsetHeight, offsetWidth.
    The values are rounded to integer numbers. They are read-only.

    These values have the same meaning with height/ width returned in Element.getBoundingClientRect() except that offset* ignores transforms.
  • offsetParent.
    Returns the reference to the parent element in the layout hierarchy.

    This value can be null in cases:
    + The element does not have an associated CSS layout box (it or its parents have a none display).
    + The element is the root element ( <html>) or <body> element.
    + The element has a fixed position. Note: Firefox returns the <body> element.
  • offsetLeft, offsetTop.
    Distances from outer border of the element to the inner border of its offsetParent element.

ElementCSSInlineStyle

  • style.
    The returned object can be modified and the change is reflected to the inline style of the element.

Window

  • scrollX, scrollY.
    Special case of Element.scrollLeft / Element.scrollTop.

    In IE, this value is rounded to integer number.
  • pageXOffset, pageYOffset.
    Alias of Window.scroll* but are read-only.
  • innerHeight, innerWidth.
    read-only values, which hold the layout dimension of the view port, including the scroll bar area (if exist). They can be changed using resizeBy()/ resizeTo().

    Note that in mobile device, when users use pinching gesture to zoom in/out, these values do not change, because the rendered document does not change.
  • outerHeight, outerWidth.
    read-only values which store the dimension of the browser window. They can be changed using resizeBy()/ resizeTo().
From MDN Web Docs
  • screenLeft = screenX, screenTop = screenY.
    read-only values which present the position of the view port related to the screen (multiple monitors are included). They can be changed using moveBy()/ moveTo().

screenX/ screenY are defined in W3 specs, while screenLeft / screenTop are implemented due to its popular usage. They are all well-supported by all major browsers.

Try this example from MDN, open it and move your browser window. The rendered circle position keeps the same when you move the browser window around.

  • resizeBy(), resizeTo().
    Used to resize the window. However, due to security reason, this API is going to be restricted. Ones should wrap the call in a try ... catch block and prepare a fallback operation in the case of failure.

    At present, Firefox requires the window created by window.open() with the 'resizable' feature, containing exactly one tab, and having the same original as the opener window . Sample code from MDN web docs:
// Create resizable window
myExternalWindow = window.open("http://myurl.domain", "myWindowName", "resizable");

// Resize window to 500x500
myExternalWindow.resizeTo(500, 500);

// Make window relatively smaller to 400x400
myExternalWindow.resizeBy(-100, -100);
  • scroll(), scrollBy(), scrollTo().
    The the associated methods of Element class in special cases.
    These methods have much higher compatibility with IE than the ones in the Element class. They are all available in IE, except that the scrollBy() method only support scrollBy(xCord, yCord) interface.
    The compatibility does not change for other major browsers.
  • moveBy(), moveTo().
    Used to move the window position. At the moment, Firefox requires
  • getComputedStyle().
    The returned object is read-only.

None-standard properties

Screen

  • availTop / availLeft.
    These are not standard properties but implemented in all major browsers except IE.

    Used to determine the total dimension of available monitors in top/left of the current monitor.
  • availHeight / availWidth.
    Screen dimension excluded the area reserved by the device/user agent, such as the dock in Unity/Mac.
From MDN Web Docs
  • pixelDepth / colorDepth.
  • height / width.
    Screen dimension including reversed area (dock, taskbar, ...). Note that this dimension is not always available for the browser to expand. Use availWidth / availHeight, instead.
  • orientation.
    Get screen orientation. Not available in Safari, Android WebView (but available in Chrome Android), Opera Android.

None-standard properties

Document

UIEvent

Because MouseEvent inherits UIEvent, MouseEvent instances also have all these properties.

These properties are all none-standard.

HTMLImage element

Forced layout thrashing and performance issue

To be continued soon...