Source: selector/Selector.js

(function() {
    /**
     * @namespace selector
     * @memberof Help4
     */
    Help4.selector = {};

    /**
     * @namespace methods
     * @memberof Help4.selector
     */
    Help4.selector.methods = {};

    /**
     * @typedef {Object} Help4.selector.methods.Selector
     * @property {string} value - the selector string
     * @property {number} quality - the selector quality
     */

    /**
     * @typedef {Object} Help4.selector.SelectorData
     * @property {string} rule - indicates which rule is used
     * @property {string|number} value - indicates the string representation of the assigned hotspot
     * @property {Help4.control2.PositionXY} [offset] - indicates the offset of the hotspot
     * @property {Help4.selector.SelectorData} [iframe] - selector values for iframe
     */

    /**
     * deliver the DOM element from given selector
     * @param {string} selector
     * @returns {NodeList<HTMLElement>|Array<void>}
     */
    Help4.selector.methods.$ = selector => {
        try {
            return Help4.selector.window.document.querySelectorAll(selector);
        } catch(e) {
        }
        return [];
    };

    /**
     * escape strings for usage with Help4.selector.methods.$
     * @param {string} value
     * @returns {string}
     */
    Help4.selector.methods.$E = value => {
        return window.CSS && CSS.escape
            ? CSS.escape(value)
            : value.replace(/([!"#$%&'()*+,-./;<=>?@[\]^`{|}~])/g, '\\$1')
                .replace(/:/g, '\\3A ')
                .replace(/\r/g, '\\d')
                .replace(/\n/g, '\\a');
    };

    /** @type {Window} */
    Help4.selector.methods.window = window;  // XRAY-708; for iframe recognition

    /**
     * @typedef {Object} Help4.selector.Rule
     * @property {string[]} blacklist - possible static position
     * @property {string[]} rules - do ignore the area of the container
     */

    /**
     * each object is of type: {@link Help4.selector.Rule}
     * @enum {Object}
     */
    Help4.selector.rules = {
        Shell: {
            blacklist: [],
            rules: ['AppFrameSelector', 'DataAttrSelector', 'ClassSelector', 'IdSelector', 'TextSelector', 'GenericSelector']
        },

        UI5: {
            blacklist: [],
            rules: ['AppFrameSelector', 'DataAttrSelector', 'ClassSelector', 'IdSelectorUI5', 'TabStripItemSelectorUI5', 'SideNavigationSelectorUI5', 'DomSelectorUI5', 'TextSelector', 'GenericSelector:unstableId']
        },

        Fiori: {
            blacklist: [],
            //rules: [/*XRAY-1666: 'BlacklistFiori', */'#UI5', 'BindingInfoUI5']
            rules: ['AppFrameSelector', 'DataAttrSelector', 'KnownElementSelector:Fiori', 'ClassSelector', 'IdSelectorUI5', 'TabStripItemSelectorUI5', 'SideNavigationSelectorUI5', 'DomSelectorUI5', 'BindingInfoUI5', 'TextSelector', 'GenericSelector:unstableId']
        },

        HTMLGUI_TOP: {
            blacklist: [],
            rules: ['AppFrameSelector', 'IframeSelector:application-', '#Fiori']
        },

        HTMLGUI_IFRAME: {
            blacklist: [],
            rules: ['DataAttrSelector', 'ClassSelector', 'HtmlGui_DialogControlSelector', 'HtmlGui_SidSelector', 'HtmlGui_IdSelector', 'NoNumbersIdSelector', 'TextSelector'/*, 'GenericSelector:unstableId'*/]
        },

        WDA_TOP: {
            blacklist: [],
            rules: ['AppFrameSelector', '#Fiori']
        },

        WDA_IFRAME: {
            blacklist: [],
            rules: ['IframeSelector', 'DataAttrSelector', 'ClassSelector', 'WDA_InputFieldSelector', 'TextSelector', 'GenericSelector:unstableId']
        }
    };

    /** Base selector handler */
    Help4.selector.Selector = class {
        static CORE_VERSION = 2;

        /**
         * some selectors have been renamed while creating xRay 2.0
         * old content still exists, need to be mapped
         * @memberof Help4.selector.Selector
         * @type {{BindingInfo: 'BindingInfoUI5', DomSelector: 'DomSelectorUI5', DataCatalogSelector: 'DataAttrSelector'}}
         */
        static LEGACY = {
            BindingInfo: 'BindingInfoUI5',
            DomSelector: 'DomSelectorUI5',
            DataCatalogSelector: 'DataAttrSelector'
        };

        /**
         * deliver the recording engine
         * @returns {Help4.engine.RecordingEngine}
         */
        static getRecordingEngine() {
            return Help4.getController().getEngine('recording');
        }

        /**
         * start recording
         * @param {Object} config
         * @param {Function} callback
         */
        static startRecording(config, callback) {
            const r = this.getRecordingEngine();
            if (r && !r.isStarted()) {
                r.start(config, selector => {
                    Help4.selector.Selector.stopRecording();
                    callback(Help4.selector.Selector.utf8ToBase64(selector));
                }, () => {
                    callback(null);
                });
            }
        }

        /** stop recording */
        static stopRecording() {
            const r = this.getRecordingEngine();
            r?.isStarted() && r.stop();
        }

        /**
         * deliver if recording engine is recording
         * @returns {boolean}
         */
        static isRecording() {
            return this.getRecordingEngine()?.isStarted() || false;
        }

        /** toggles the recording info area */
        static toggleRecordingInfo() {
            this.getRecordingEngine()?.toggleInfoArea();
        }

        /** toggles the recording via recording engine */
        static toggleRecording() {
            this.getRecordingEngine()?.toggleRecording();
        }

        /**
         * convert and deliver given base64 string to utf8
         * @param {string} str
         * @returns {?Object}
         */
        static base64ToUtf8(str) {
            if (typeof str === 'string') {
                const {JSON, selector: {Selector: {CORE_VERSION}}} = Help4;

                const x = str.match(/WA#(\d+)#(.*$)/);
                const version = x ? Number(x[1]) : -1;

                try {
                    return version > 0 && version <= CORE_VERSION
                        ? JSON.parse(decodeURIComponent(window.atob(x[2])))  // new selector versions encode the data string
                        : JSON.parse(window.atob(str));  // XRAY-1826: version 1 did not encode; decode would destroy the data
                } catch(e) {
                }

                return null;
            }
            return str;
        }

        /**
         * convert and deliver given utf8 selector to base64 string
         * @param {Help4.selector.SelectorData} selector
         * @returns {?string}
         */
        static utf8ToBase64(selector) {
            try {
                // encoding is needed as btoa will break on special chars
                const str = window.btoa(encodeURIComponent(Help4.JSON.stringify(selector)));
                return 'WA#' + Help4.selector.Selector.CORE_VERSION + '#' + str;
            } catch(e) {
            }
            return null;
        }

        /**
         * deliver the rule for a given selector
         * @param {Help4.selector.SelectorData} selector
         * @returns {?string}
         */
        static getRule(selector) {
            return selector?.rule
                ? Help4.selector.Selector.LEGACY[selector.rule] || selector.rule
                : null;
        }
    };
})();