diff --git a/javascript/atoms/test/attribute_typescript_test.html b/javascript/atoms/test/attribute_typescript_test.html index a795cb78187a3..24f36dcbf6d6d 100644 --- a/javascript/atoms/test/attribute_typescript_test.html +++ b/javascript/atoms/test/attribute_typescript_test.html @@ -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) { diff --git a/javascript/atoms/test/shown_typescript_test.html b/javascript/atoms/test/shown_typescript_test.html index 3a0be2c583592..c59544fd36499 100644 --- a/javascript/atoms/test/shown_typescript_test.html +++ b/javascript/atoms/test/shown_typescript_test.html @@ -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'))); }); diff --git a/javascript/atoms/typescript/get-attribute.ts b/javascript/atoms/typescript/get-attribute.ts index 47d539ab72925..9af14941c8533 100644 --- a/javascript/atoms/typescript/get-attribute.ts +++ b/javascript/atoms/typescript/get-attribute.ts @@ -21,7 +21,7 @@ 'readonly': 'readOnly', }; - const BOOLEAN_PROPERTIES: string[] = [ + const BOOLEAN_PROPERTIES: Set = new Set([ 'allowfullscreen', 'allowpaymentrequest', 'allowusermedia', @@ -68,7 +68,7 @@ 'truespeed', 'typemustmatch', 'willvalidate', - ]; + ]); function getAttribute(element: Element, attributeName: string): string | null { return element.getAttribute(attributeName.toLowerCase()); @@ -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; } @@ -179,7 +179,7 @@ } if (property == null || isObject(property)) { - const attrValue = getAttribute(element, attribute); + const attrValue = element.getAttribute(name); return attrValue != null ? attrValue : null; } diff --git a/javascript/atoms/typescript/is-displayed.ts b/javascript/atoms/typescript/is-displayed.ts index 238088b526e26..8510baf82c201 100644 --- a/javascript/atoms/typescript/is-displayed.ts +++ b/javascript/atoms/typescript/is-displayed.ts @@ -45,6 +45,13 @@ interface Coordinate { // Guards against form children that shadow tagName (e.g.
). 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(); + var clientRectCache = new Map(); + var displayedCache = new Map(); + function toUpperCaseTag(tagName?: string): string | undefined { return tagName ? tagName.toUpperCase() : undefined; } @@ -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; } @@ -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 { @@ -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 { @@ -459,10 +471,16 @@ 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; } } @@ -470,20 +488,25 @@ interface Coordinate { 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') && !(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);