(function() {
/**
* @enum {number}
*/
Help4.selector.SELECTOR_QUALITY = {
best: 1, // DataAttrSelector
good: 2, // "safe" selectors
lang: 3, // "safe" but language dependent
other: 10 // "unsafe"
};
/**
* @enum {number}
*/
Help4.selector.SELECTOR_SCORES = {
safe: 1.6,
good: 1.4,
lang: 1.2,
unsafe: 1,
aria: 1.1
};
Help4.selector.methods._CACHE = {};
/**
* @typedef {Object} Help4.selector.methods.Utils.Config
* @property {number} depthThreshold
* @property {number} desiredMinLength
* @property {boolean} enabled
* @property {string[]} filter
* @property {boolean} ignoreNumbers
* @property {string[]} invalidClasses
* @property {number} minLength
* @property {string[]} tags
* @property {Help4.control2.SizeWidthHeight} textSize
* @property {number} truncationLength
*/
/**
* @typedef {Object} Help4.selector.methods.Utils.Config2
* @property {boolean} enabled
* @property {Array<{tagName: string}>} elements
* @property {number} searchLevel
* @property {string[]} skippedTags
*/
/** Util class for selector methods */
Help4.selector.methods.Utils = class {
/**
* @param {string} selector
* @returns {?HTMLElement}
*/
static getElement(selector) {
const elements = Help4.selector.methods.$(selector);
for (const element of elements) {
if (Help4.Element.isVisible(element)) return element; // return the 1st visible one
}
return elements[0] || null; // none is visible; return the 1st
}
/** record selector */
static record() {
Help4.API._stopRecording = function() {
Help4.selector.Selector.stopRecording();
};
const controller = Help4.getController();
let s = controller.getSelector() || {};
s = Help4.jsonClone(s);
// only run urEngine closed if panel is not already open
const {isOpen} = controller.getConfiguration();
const urEngine = isOpen ? null : controller.getEngine('urHarmonization');
const previous = urEngine?.runClosed();
Help4.selector.Selector.startRecording(s, selector => {
const decoded = Help4.selector.Selector.base64ToUtf8(selector);
console.log(decoded);
// queue after this thread / fired events (hotspotAssignStop)
setTimeout(() => {
Help4.API._stopRecording();
urEngine?.runClosed(previous);
}, 1);
});
Help4.selector.Selector.toggleRecordingInfo();
}
/**
* @param {Help4.selector.methods.Utils.Config} config
* @param {HTMLElement} elem
* @param {string} expectedText
* @returns {?string}
*/
static matchesText(config, elem, expectedText) {
if (!config.depthThreshold) config.depthThreshold = 0;
let extractedText = this.extractText(config, elem);
const {text, depth} = _getTextInfo.call(this, extractedText);
const min = Math.max(0, depth - config.depthThreshold);
const max = depth + config.depthThreshold;
const {text: eText, depth: eDepth} = _getTextInfo.call(this, expectedText);
return text === eText && Help4.isPointInRange(eDepth, min, max) ? expectedText : '';
}
/**
* IE11 compatibility.
* checks if element matches the selector
* @param {HTMLElement} elem
* @param {string} selector
* @returns {boolean}
*/
static matchesSelector(elem, selector) {
const m = elem.matches || elem.msMatchesSelector || elem.webkitMatchesSelector;
return m ? m.call(elem, selector) : false;
}
/**
* @param {Help4.selector.methods.Utils.Config} config
* @param {HTMLElement} elem
* @returns {string}
*/
static extractText(config, elem) {
if (Help4.control.Store.find(elem) || Help4.control2.Store.find(elem)) return ''; // ignore our own DOM
const {enabled, tags, truncationLength} = config;
if (enabled !== false && (!tags || tags.indexOf(elem.tagName) >= 0)) {
let v = _getFirstNodeValue.call(this, config, elem);
if (v && truncationLength > 0) {
const va = v.split(' '); // white space is delimiter for value and depth; see _getFirstNodeValue
const de = va.pop(); // last item is depth
let txt = va.join(''); // merge all texts
if (txt.length > truncationLength) {
txt = txt.substring(0, truncationLength);
const out = [];
for (const t of va) {
if (txt.length >= t.length) {
out.push(txt.substring(0, t.length));
txt = txt.substring(t.length);
if (!txt) break;
} else {
out.push(txt);
break;
}
}
v = out.join(' ') + ' ' + de;
}
}
return v;
}
return '';
}
/**
* extracts href (full url) from given element
* @param {{enabled: boolean, tags: string[], useQuery: boolean, useHash: boolean, invalidQueryKeys: string[]}} config
* @param {HTMLElement} elem
* @returns {string}
*/
static extractHref(config, elem) {
const {enabled, tags, useHash, useQuery, invalidQueryKeys} = config;
if (enabled === false || tags && tags.indexOf(elem.tagName) < 0) return '';
// getAttribute will return local href, such as "/img/img1.png"
// while elem.href will always return absolute urls
// absolute urls do not match with querySelectorAll
const v = elem.getAttribute('href') || elem.href;
if (!v || typeof v !== 'string') return '';
let xq = v.indexOf('?');
const xh = v.indexOf('#');
if (xh > 0 && xq > xh) xq = -1; // fiori: query strings within hash
const p = [];
if (xh > 0 && useHash) {
p.push(v.substring(xh));
}
if (xq > 0 && useQuery) {
const q = v.substring(xq + 1, xh > 0 ? xh : undefined).split('&');
const r = [];
for (const k of q) {
if (invalidQueryKeys.indexOf(k.split('=')[0]) < 0) r.push(k);
}
p.unshift('?' + r.sort().join('&'));
}
let x = -1;
if (xq > 0 && xh > 0) {
x = Math.min(xq, xh);
} else if (xq > 0 || xh > 0) {
x = Math.max(xq, xh);
}
p.unshift(x > 0 ? v.substring(0, x) : v);
return p.join('');
}
/**
* extracts src (reduced url) from given element
* @param {{enabled: boolean}} config
* @param {HTMLElement} elem
* @returns {string}
*/
static extractSrc(config, elem) {
if (config.enabled === false) return '';
let v = elem.src || elem.getAttribute('src');
if (!v || typeof v !== 'string') return '';
if (v) {
const x = v.lastIndexOf('/');
v = v.substring(x < 0 ? 0 : x + 1).replace(/_hover/, '');
}
return v;
}
/**
* @param {string} type
* @param {string} url
* @returns {string[]}
*/
static createSmartUrl(type, url) {
// href: _extractHref provides full url
// src: _extractSrc provides reduced url
const first = type === 'href' ? '^' : '*';
let c = url.split(/\?|#/);
const s = ['[' + type + first + '="' + Help4.selector.methods.$E(c.shift()) + '"]'];
if (url.indexOf('#') >= 0) {
s.push('[' + type + '$="' + Help4.selector.methods.$E('#' + c.pop()) + '"]');
}
if (url.indexOf('?') >= 0) {
c = c[0].split('&');
for (const part of c) {
s.push('[' + type + '*="' + Help4.selector.methods.$E(part) + '"]');
}
}
return s;
}
/**
* @param {Help4.selector.methods.Utils.Config2} config
* @param {HTMLElement} elem
* @returns {HTMLElement}
*/
static improveInitialElement(config, elem) {
const {enabled, skippedTags, searchLevel} = config;
if (enabled === false) return elem;
let e = skippedTags.indexOf(elem.tagName) >= 0 ? Help4.Element.getParent(elem, true) : elem;
let i = 0;
while (e && i++ < searchLevel) {
if (_isGroupElement.call(this, config, e)) return e;
e = Help4.Element.getParent(e, true);
}
return elem;
}
};
/**
* delivers text info
* @memberof Help4.selector.methods.Utils
* @private
* @param {string} text
* @returns {{depth: number, text: string}}
*/
function _getTextInfo(text) {
text = text.split(' ');
return {
depth: Number(text.pop()), // depth of text relative to node
text: text.join(' ') // extracted text
};
}
/**
* @memberof Help4.selector.methods.Utils
* @private
* @param {Help4.selector.methods.Utils.Config} config
* @param {HTMLElement} elem
* @returns {string}
*/
function _getFirstNodeValue(config, elem) {
const value = [];
const depth = [];
let {filter, textSize, invalidClasses, ignoreNumbers, desiredMinLength} = config;
if (invalidClasses) invalidClasses = invalidClasses.join(' ').toLowerCase().split(' ');
const css = e => {
const cn = e.className.toLowerCase().trim().replace(/\s+/g, ' ').split(' ');
for (const c of cn) {
if (invalidClasses.indexOf(c) >= 0) return false;
}
return true;
}
const scan = (e, d) => {
// check for invalid classes
if (invalidClasses && !css(e)) return false;
const {childNodes, offsetWidth, offsetHeight} = e;
const d1 = d + 1;
for (const n of childNodes) {
let v;
const {nodeName, nodeType, nodeValue} = n;
if (nodeType === 1) { // element node
if (nodeName === 'svg') continue; // XRAY-2080; looking into svg not supported
if (!filter || filter.indexOf(nodeName) >= 0) { // only scan if allowed by whitelist filter
if (v = scan(n, d1)) return true;
}
} else if (nodeType === 3) { // text node
// text size restrictions to avoid recording of invisible texts
if (textSize && (offsetWidth < textSize.width || offsetHeight < textSize.height)) continue;
v = nodeValue.replace(/\s/g, ''); // remove all whitespaces
if (ignoreNumbers) v = v.replace(/\d/g, '#'); // remove all numbers
if (v) {
value.push(v);
depth.push(d);
// value is considered to be long enough
if (value.join('').length >= desiredMinLength) return true;
}
}
}
return false;
}
// case 1: desiredMinLength is reached; all fine
// case 2: desiredMinLength not reached but at least minLength is reached
return scan(elem, 0) || value.length && value.join('').length >= config.minLength
// white space is removed from value; therefore free to use as delimiter
? value.join(' ') + ' ' + depth[0]
: '';
}
/**
* @memberof Help4.selector.methods.Utils
* @private
* @param {Help4.selector.methods.Utils.Config2} config
* @param {HTMLElement} elem
* @returns {boolean}
*/
function _isGroupElement(config, elem) {
for (const c of config.elements) {
for (const k in c) {
if (c.hasOwnProperty(k) && c[k] !== elem[k]) return false;
}
}
return true;
}
})();