Created
April 23, 2021 09:03
-
-
Save bcaudan/04b4ceefcbd752251a848341eb817897 to your computer and use it in GitHub Desktop.
display_action_name.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
function getActionNameFromElement(element) { | |
// Proceed to get the action name in two steps: | |
// * first, get the name programmatically, explicitly defined by the user. | |
// * then, use strategies that are known to return good results. Those strategies will be used on | |
// the element and a few parents, but it's likely that they won't succeed at all. | |
// * if no name is found this way, use strategies returning less accurate names as a fallback. | |
// Those are much likely to succeed. | |
return (getActionNameFromElementProgrammatically(element) || | |
getActionNameFromElementForStrategies(element, priorityStrategies) || | |
getActionNameFromElementForStrategies(element, fallbackStrategies) || | |
''); | |
} | |
/** | |
* Get the action name from the attribute 'data-dd-action-name' on the element or any of its parent. | |
*/ | |
var PROGRAMMATIC_ATTRIBUTE = 'data-dd-action-name'; | |
function getActionNameFromElementProgrammatically(targetElement) { | |
var elementWithAttribute; | |
// We don't use getActionNameFromElementForStrategies here, because we want to consider all parents, | |
// without limit. It is up to the user to declare a relevant naming strategy. | |
// If available, use element.closest() to match get the attribute from the element or any of its | |
// parent. Else fallback to a more traditional implementation. | |
if (supportsElementClosest()) { | |
elementWithAttribute = targetElement.closest("[" + PROGRAMMATIC_ATTRIBUTE + "]"); | |
} | |
else { | |
var element = targetElement; | |
while (element) { | |
if (element.hasAttribute(PROGRAMMATIC_ATTRIBUTE)) { | |
elementWithAttribute = element; | |
break; | |
} | |
element = element.parentElement; | |
} | |
} | |
if (!elementWithAttribute) { | |
return; | |
} | |
var name = elementWithAttribute.getAttribute(PROGRAMMATIC_ATTRIBUTE); | |
return truncate(normalizeWhitespace(name.trim())); | |
} | |
var priorityStrategies = [ | |
// associated LABEL text | |
function (element) { | |
// IE does not support element.labels, so we fallback to a CSS selector based on the element id | |
// instead | |
if (supportsLabelProperty()) { | |
if ('labels' in element && element.labels && element.labels.length > 0) { | |
return getTextualContent(element.labels[0]); | |
} | |
} | |
else if (element.id) { | |
var label = element.ownerDocument && element.ownerDocument.querySelector("label[for=\"" + element.id.replace('"', '\\"') + "\"]"); | |
return label && getTextualContent(label); | |
} | |
}, | |
// INPUT button (and associated) value | |
function (element) { | |
if (element.nodeName === 'INPUT') { | |
var input = element; | |
var type = input.getAttribute('type'); | |
if (type === 'button' || type === 'submit' || type === 'reset') { | |
return input.value; | |
} | |
} | |
}, | |
// BUTTON, LABEL or button-like element text | |
function (element) { | |
if (element.nodeName === 'BUTTON' || element.nodeName === 'LABEL' || element.getAttribute('role') === 'button') { | |
return getTextualContent(element); | |
} | |
}, | |
function (element) { return element.getAttribute('aria-label'); }, | |
// associated element text designated by the aria-labelledby attribute | |
function (element) { | |
var labelledByAttribute = element.getAttribute('aria-labelledby'); | |
if (labelledByAttribute) { | |
return labelledByAttribute | |
.split(/\s+/) | |
.map(function (id) { return getElementById(element, id); }) | |
.filter(function (label) { return Boolean(label); }) | |
.map(getTextualContent) | |
.join(' '); | |
} | |
}, | |
function (element) { return element.getAttribute('alt'); }, | |
function (element) { return element.getAttribute('name'); }, | |
function (element) { return element.getAttribute('title'); }, | |
function (element) { return element.getAttribute('placeholder'); }, | |
// SELECT first OPTION text | |
function (element) { | |
if ('options' in element && element.options.length > 0) { | |
return getTextualContent(element.options[0]); | |
} | |
}, | |
]; | |
var fallbackStrategies = [function (element) { return getTextualContent(element); }]; | |
/** | |
* Iterates over the target element and its parent, using the strategies list to get an action name. | |
* Each strategies are applied on each element, stopping as soon as a non-empty value is returned. | |
*/ | |
var MAX_PARENTS_TO_CONSIDER = 10; | |
function getActionNameFromElementForStrategies(targetElement, strategies) { | |
var element = targetElement; | |
var recursionCounter = 0; | |
while (recursionCounter <= MAX_PARENTS_TO_CONSIDER && | |
element && | |
element.nodeName !== 'BODY' && | |
element.nodeName !== 'HTML' && | |
element.nodeName !== 'HEAD') { | |
for (var _i = 0, strategies_1 = strategies; _i < strategies_1.length; _i++) { | |
var strategy = strategies_1[_i]; | |
var name_1 = strategy(element); | |
if (typeof name_1 === 'string') { | |
var trimmedName = name_1.trim(); | |
if (trimmedName) { | |
return truncate(normalizeWhitespace(trimmedName)); | |
} | |
} | |
} | |
// Consider a FORM as a contextual limit to get the action name. This is experimental and may | |
// be reconsidered in the future. | |
if (element.nodeName === 'FORM') { | |
break; | |
} | |
element = element.parentElement; | |
recursionCounter += 1; | |
} | |
} | |
function normalizeWhitespace(s) { | |
return s.replace(/\s+/g, ' '); | |
} | |
function truncate(s) { | |
return s.length > 100 ? s.substring(0, 100) + " [...]" : s; | |
} | |
function getElementById(refElement, id) { | |
// Use the element ownerDocument here, because tests are executed in an iframe, so | |
// document.getElementById won't work. | |
return refElement.ownerDocument ? refElement.ownerDocument.getElementById(id) : null; | |
} | |
function getTextualContent(element) { | |
if (element.isContentEditable) { | |
return; | |
} | |
if ('innerText' in element) { | |
var text = element.innerText; | |
if (!supportsInnerTextScriptAndStyleRemoval()) { | |
// remove the inner text of SCRIPT and STYLES from the result. This is a bit dirty, but should | |
// be relatively fast and work in most cases. | |
var elementsTextToRemove = element.querySelectorAll('script, style'); | |
// eslint-disable-next-line @typescript-eslint/prefer-for-of | |
for (var i = 0; i < elementsTextToRemove.length; i += 1) { | |
var innerText = elementsTextToRemove[i].innerText; | |
if (innerText.trim().length > 0) { | |
text = text.replace(innerText, ''); | |
} | |
} | |
} | |
return text; | |
} | |
return element.textContent; | |
} | |
/** | |
* Returns true if element.innerText excludes the text from inline SCRIPT and STYLE element. This | |
* should be the case everywhere except on some version of Internet Explorer. | |
* See http://perfectionkills.com/the-poor-misunderstood-innerText/#diff-with-textContent | |
*/ | |
var supportsInnerTextScriptAndStyleRemovalResult; | |
function supportsInnerTextScriptAndStyleRemoval() { | |
if (supportsInnerTextScriptAndStyleRemovalResult === undefined) { | |
var style = document.createElement('style'); | |
style.textContent = '*'; | |
var div = document.createElement('div'); | |
div.appendChild(style); | |
document.body.appendChild(div); | |
supportsInnerTextScriptAndStyleRemovalResult = div.innerText === ''; | |
document.body.removeChild(div); | |
} | |
return supportsInnerTextScriptAndStyleRemovalResult; | |
} | |
/** | |
* Returns true if the browser supports the element.labels property. This should be the case | |
* everywhere except on Internet Explorer. | |
* Note: The result is computed lazily, because we don't want any DOM access when the SDK is | |
* evaluated. | |
*/ | |
var supportsLabelPropertyResult; | |
function supportsLabelProperty() { | |
if (supportsLabelPropertyResult === undefined) { | |
supportsLabelPropertyResult = 'labels' in HTMLInputElement.prototype; | |
} | |
return supportsLabelPropertyResult; | |
} | |
/** | |
* Returns true if the browser supports the element.closest method. This should be the case | |
* everywhere except on Internet Explorer. | |
* Note: The result is computed lazily, because we don't want any DOM access when the SDK is | |
* evaluated. | |
*/ | |
var supportsElementClosestResult; | |
function supportsElementClosest() { | |
if (supportsElementClosestResult === undefined) { | |
supportsElementClosestResult = 'closest' in HTMLElement.prototype; | |
} | |
return supportsElementClosestResult; | |
} | |
const span = document.createElement('span') | |
span.setAttribute('style', "position: fixed; background-color: red; top: 0; z-index: 100") | |
document.body.append(span) | |
document.addEventListener('mousemove', (event) => { | |
span.textContent = getActionNameFromElement(event.target) | |
}) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment