You can calculate the user-applied (non-default) styles by comparing them against a "default" HTML element of the same tag name, which is rendered in an isolated <iframe>
so no styles from the document "leak" into the default element.
This solution is the same as @Just a student, but adds these improvements:
- the
<iframe>
is a hidden HTML element so the user won't see it
- for performance, default styles are cached and we wait to clean up the
<iframe>
until the end when we call removeSandbox
- it accounts for inheritance (i.e. if you provide
parentElement
it will list a style even when the parent sets it and the element overrides it back to the default)
- it accounts for situations where a default style's initial value and computed value don't match (for more info, see note [1] in this PR)
// usage:
element = document.querySelector('div');
styles = getUserComputedStyles(element);
styles = getUserComputedStyles(element, parentElement);
// call this method when done to cleanup:
removeSandbox();
function getUserComputedStyles(element, parentElement = null) {
var defaultStyle = getDefaultStyle(element.tagName);
var computedStyle = window.getComputedStyle(element);
var parentStyle =
parentElement ? window.getComputedStyle(parentElement) : null;
var styles = {};
[...computedStyle].forEach(function(name) {
// If the style does not match the default, or it does not match the
// parent's, set it. We don't know which styles are inherited from the
// parent and which aren't, so we have to always check both.
// This results in some extra default styles being returned, so if you
// want to avoid this and aren't concerned about omitting styles that
// the parent set but the `element` overrides back to the default,
// call `getUserComputedStyles` without a `parentElement`.
const computedStyleValue = computedStyle[name];
if (computedStyleValue !== defaultStyle[name] ||
(parentStyle && computedStyleValue !== parentStyle[name])) {
styles[name] = computedStyleValue;
}
});
return styles;
}
var removeDefaultStylesTimeoutId = null;
var sandbox = null;
var tagNameDefaultStyles = {};
function getDefaultStyle(tagName) {
if (tagNameDefaultStyles[tagName]) {
return tagNameDefaultStyles[tagName];
}
if (!sandbox) {
// Create a hidden sandbox <iframe> element within we can create
// default HTML elements and query their computed styles. Elements
// must be rendered in order to query their computed styles. The
// <iframe> won't render at all with `display: none`, so we have to
// use `visibility: hidden` with `position: fixed`.
sandbox = document.createElement('iframe');
sandbox.style.visibility = 'hidden';
sandbox.style.position = 'fixed';
document.body.appendChild(sandbox);
// Ensure that the iframe is rendered in standard mode
sandbox.contentWindow.document.write(
'<!DOCTYPE html><meta charset="UTF-8"><title>sandbox</title><body>');
}
var defaultElement = document.createElement(tagName);
sandbox.contentWindow.document.body.appendChild(defaultElement);
// Ensure that there is some content, so properties like margin are applied
defaultElement.textContent = '.';
var defaultComputedStyle =
sandbox.contentWindow.getComputedStyle(defaultElement);
var defaultStyle = {};
// Copy styles to an object, making sure that 'width' and 'height' are
// given the default value of 'auto', since their initial value is always
// 'auto' despite that the default computed value is sometimes an absolute
// length.
[...defaultComputedStyle].forEach(function(name) {
defaultStyle[name] = (name === 'width' || name === 'height')
? 'auto' : defaultComputedStyle.getPropertyValue(name);
});
sandbox.contentWindow.document.body.removeChild(defaultElement);
tagNameDefaultStyles[tagName] = defaultStyle;
return defaultStyle;
}
function removeSandbox() {
if (!sandbox) {
return;
}
document.body.removeChild(sandbox);
sandbox = null;
if (removeDefaultStylesTimeoutId) {
clearTimeout(removeDefaultStylesTimeoutId);
}
removeDefaultStylesTimeoutId = setTimeout(() => {
removeDefaultStylesTimeoutId = null;
tagNameDefaultStyles = {};
}, 20 * 1000);
}
Even with these improvements, for block elements, some default styles are still listed because their initial and computed values don't match, namely width
, height
, block-size
, inset-block
, transform-origin
, and perspective-origin
(see note in #4). This solution in dom-to-image-more (the getUserComputedStyle
function) is able to trim away even more of these, although the calculation is slower.