Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions javascript/atoms/test/attribute_typescript_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@
assertAttributeEquals(assert, 'lovely', byId('cheddar'), 'unknown');
});

QUnit.test('boolean property hidden returns true when attribute present', function (assert) {
var el = document.createElement('div');
el.setAttribute('hidden', '');
assertAttributeEquals(assert, 'true', el, 'hidden');
});

QUnit.test('boolean property multiple returns true when attribute present', function (assert) {
var el = document.createElement('select');
el.setAttribute('multiple', '');
assertAttributeEquals(assert, 'true', el, 'multiple');
});

// --- event handler attributes ---

QUnit.test('event handler attribute falls back to getAttribute, not function stringification', function (assert) {
Expand Down
44 changes: 44 additions & 0 deletions javascript/atoms/test/shown_typescript_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,50 @@
assert.notOk(isShown(byId('orphanArea')));
});

QUnit.test('map with hyphen in name is found via querySelector', function (assert) {
var img = document.createElement('img');
img.setAttribute('usemap', '#my-map');
img.setAttribute('width', '100');
img.setAttribute('height', '100');
document.body.appendChild(img);

var map = document.createElement('map');
map.setAttribute('name', 'my-map');
var area = document.createElement('area');
area.setAttribute('shape', 'rect');
area.setAttribute('coords', '0,0,50,50');
map.appendChild(area);
document.body.appendChild(map);

assert.ok(isShown(map), 'map linked to visible image should be shown');
assert.ok(isShown(area), 'area linked to visible image should be shown');

document.body.removeChild(img);
document.body.removeChild(map);
});

QUnit.test('map with double-quote in name is found via querySelector', function (assert) {
var img = document.createElement('img');
img.setAttribute('width', '100');
img.setAttribute('height', '100');
document.body.appendChild(img);

var map = document.createElement('map');
map.setAttribute('name', 'my"map');
img.setAttribute('usemap', '#my"map');

var area = document.createElement('area');
area.setAttribute('shape', 'rect');
area.setAttribute('coords', '0,0,50,50');
map.appendChild(area);
document.body.appendChild(map);

assert.ok(isShown(map), 'map with quote in name linked to visible image should be shown');

document.body.removeChild(img);
document.body.removeChild(map);
});

QUnit.test('element with nested block level element shown', function (assert) {
assert.ok(isShown(byId('containsNestedBlock')));
});
Expand Down
12 changes: 6 additions & 6 deletions javascript/atoms/typescript/get-attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
'readonly': 'readOnly',
};

const BOOLEAN_PROPERTIES: string[] = [
const BOOLEAN_PROPERTIES: Set<string> = new Set([
'allowfullscreen',
'allowpaymentrequest',
'allowusermedia',
Expand Down Expand Up @@ -68,7 +68,7 @@
'truespeed',
'typemustmatch',
'willvalidate',
];
]);

function getAttribute(element: Element, attributeName: string): string | null {
return element.getAttribute(attributeName.toLowerCase());
Expand Down Expand Up @@ -160,14 +160,14 @@

const propName = PROPERTY_ALIASES[name] || attribute;

if (BOOLEAN_PROPERTIES.indexOf(name) !== -1) {
const hasAttr = getAttribute(element, attribute) !== null;
if (BOOLEAN_PROPERTIES.has(name)) {
const hasAttr = element.getAttribute(name) !== null;
const propValue = getProperty(element, propName);
return hasAttr || !!propValue ? 'true' : null;
}

if (name === 'value' && isElement(element, 'LI')) {
const attrValue = getAttribute(element, attribute);
const attrValue = element.getAttribute(name);
return attrValue != null ? attrValue : null;
}

Expand All @@ -179,7 +179,7 @@
}

if (property == null || isObject(property)) {
const attrValue = getAttribute(element, attribute);
const attrValue = element.getAttribute(name);
return attrValue != null ? attrValue : null;
}

Expand Down
97 changes: 60 additions & 37 deletions javascript/atoms/typescript/is-displayed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ interface Coordinate {
// Guards against form children that shadow tagName (e.g. <form><input name="tagName">).
var tagNameDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'tagName');

// Per-call memoisation caches. Scoped here so they live only for the
// duration of this synchronous call — no stale-data risk, no GC pressure
// between invocations.
var computedStyleCache = new Map<Element, CSSStyleDeclaration | null>();
var clientRectCache = new Map<Element, Rect>();
var displayedCache = new Map<Node, boolean>();

function toUpperCaseTag(tagName?: string): string | undefined {
return tagName ? tagName.toUpperCase() : undefined;
}
Expand Down Expand Up @@ -81,11 +88,12 @@ interface Coordinate {
}

function getEffectiveStyle(elem: Element, propertyName: string): string | null {
var win = elem.ownerDocument.defaultView;
if (!win) {
return null;
var computed = computedStyleCache.get(elem);
if (computed === undefined) {
var win = elem.ownerDocument.defaultView;
computed = win ? (win.getComputedStyle(elem) || null) : null;
computedStyleCache.set(elem, computed);
}
var computed = win.getComputedStyle(elem);
if (!computed) {
return null;
}
Expand Down Expand Up @@ -124,33 +132,42 @@ interface Coordinate {
}

function getClientRect(elem: Element): Rect {
var cachedRect = clientRectCache.get(elem);
if (cachedRect) {
return cachedRect;
}

var rect: Rect;
var imageMap = maybeFindImageMap(elem);
if (imageMap) {
return imageMap.rect;
}

var elemTagName = typeof (elem as Element).tagName === 'string' ? (elem as Element).tagName : '';
if (elemTagName.toUpperCase() === 'HTML') {
var doc = (elem as Element).ownerDocument;
// In quirks mode (no DOCTYPE), viewport dimensions come from document.body;
// documentElement.clientWidth/Height is unreliable and can be 0.
var sizeElem = doc.compatMode === 'CSS1Compat' ? doc.documentElement : (doc.body || doc.documentElement);
return createRect(0, 0, sizeElem.clientWidth, sizeElem.clientHeight);
}

try {
var nativeRect = (elem as Element).getBoundingClientRect();
return {
left: nativeRect.left,
top: nativeRect.top,
right: nativeRect.right,
bottom: nativeRect.bottom,
width: nativeRect.right - nativeRect.left,
height: nativeRect.bottom - nativeRect.top,
};
} catch (_error) {
return createRect(0, 0, 0, 0);
rect = imageMap.rect;
} else {
var elemTagName = typeof (elem as Element).tagName === 'string' ? (elem as Element).tagName : '';
if (elemTagName.toUpperCase() === 'HTML') {
var doc = (elem as Element).ownerDocument;
// In quirks mode (no DOCTYPE), viewport dimensions come from document.body;
// documentElement.clientWidth/Height is unreliable and can be 0.
var sizeElem = doc.compatMode === 'CSS1Compat' ? doc.documentElement : (doc.body || doc.documentElement);
rect = createRect(0, 0, sizeElem.clientWidth, sizeElem.clientHeight);
} else {
try {
var nativeRect = (elem as Element).getBoundingClientRect();
rect = {
left: nativeRect.left,
top: nativeRect.top,
right: nativeRect.right,
bottom: nativeRect.bottom,
width: nativeRect.right - nativeRect.left,
height: nativeRect.bottom - nativeRect.top,
};
} catch (_error) {
rect = createRect(0, 0, 0, 0);
}
}
}

clientRectCache.set(elem, rect);
return rect;
}

function getAreaRelativeRect(area: HTMLAreaElement): Rect {
Expand Down Expand Up @@ -187,14 +204,9 @@ interface Coordinate {
}

function findImageUsingMap(mapName: string, doc: Document): Element | null {
var elements = doc.getElementsByTagName('*');
for (var index = 0; index < elements.length; index += 1) {
var useMap = elements[index].getAttribute('usemap');
if (useMap === '#' + mapName) {
return elements[index];
}
}
return null;
// Use querySelector instead of a full-DOM scan; escape the map name so
// special characters in the CSS attribute value string are handled safely.
return doc.querySelector('[usemap="#' + mapName.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"]');
}

function maybeFindImageMap(elem: Element): ImageMapResult | null {
Expand Down Expand Up @@ -459,31 +471,42 @@ interface Coordinate {
}

function displayed(node: Node): boolean {
var cached = displayedCache.get(node);
if (cached !== undefined) {
return cached;
}

if (isElement(node)) {
var display = getEffectiveStyle(node, 'display');
var contentVisibility = getEffectiveStyle(node, 'content-visibility');
if (display === 'none' || contentVisibility === 'hidden') {
displayedCache.set(node, false);
return false;
}
}

var parent = getParentNodeInComposedDom(node);
if (typeof ShadowRoot === 'function' && parent instanceof ShadowRoot) {
if (parent.host.shadowRoot && parent.host.shadowRoot !== parent) {
displayedCache.set(node, false);
return false;
}
parent = parent.host;
}

if (parent && (parent.nodeType === Node.DOCUMENT_NODE || parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE)) {
displayedCache.set(node, true);
return true;
}

if (isElement(parent, 'DETAILS') && !(<HTMLDetailsElement>parent).open && !isElement(node, 'SUMMARY')) {
displayedCache.set(node, false);
return false;
}

return !!parent && displayed(parent);
var result = !!parent && displayed(parent);
displayedCache.set(node, result);
return result;
}

return isShownInternal(elem, !!optIgnoreOpacity, displayed);
Expand Down
Loading