"use strict";
/**
 * Methods for getting and modifying attributes.
 *
 * @module cheerio/attributes
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.toggleClass = exports.removeClass = exports.addClass = exports.hasClass = exports.removeAttr = exports.val = exports.data = exports.prop = exports.attr = void 0;
var static_js_1 = require("../static.js");
var utils_js_1 = require("../utils.js");
var domutils_1 = require("domutils");
var hasOwn = Object.prototype.hasOwnProperty;
var rspace = /\s+/;
var dataAttrPrefix = 'data-';
/*
 * Lookup table for coercing string data-* attributes to their corresponding
 * JavaScript primitives
 */
var primitives = {
    null: null,
    true: true,
    false: false,
};
// Attributes that are booleans
var rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i;
// Matches strings that look like JSON objects or arrays
var rbrace = /^{[^]*}$|^\[[^]*]$/;
function getAttr(elem, name, xmlMode) {
    var _a;
    if (!elem || !(0, utils_js_1.isTag)(elem))
        return undefined;
    (_a = elem.attribs) !== null && _a !== void 0 ? _a : (elem.attribs = {});
    // Return the entire attribs object if no attribute specified
    if (!name) {
        return elem.attribs;
    }
    if (hasOwn.call(elem.attribs, name)) {
        // Get the (decoded) attribute
        return !xmlMode && rboolean.test(name) ? name : elem.attribs[name];
    }
    // Mimic the DOM and return text content as value for `option's`
    if (elem.name === 'option' && name === 'value') {
        return (0, static_js_1.text)(elem.children);
    }
    // Mimic DOM with default value for radios/checkboxes
    if (elem.name === 'input' &&
        (elem.attribs['type'] === 'radio' || elem.attribs['type'] === 'checkbox') &&
        name === 'value') {
        return 'on';
    }
    return undefined;
}
/**
 * Sets the value of an attribute. The attribute will be deleted if the value is `null`.
 *
 * @private
 * @param el - The element to set the attribute on.
 * @param name - The attribute's name.
 * @param value - The attribute's value.
 */
function setAttr(el, name, value) {
    if (value === null) {
        removeAttribute(el, name);
    }
    else {
        el.attribs[name] = "".concat(value);
    }
}
function attr(name, value) {
    // Set the value (with attr map support)
    if (typeof name === 'object' || value !== undefined) {
        if (typeof value === 'function') {
            if (typeof name !== 'string') {
                {
                    throw new Error('Bad combination of arguments.');
                }
            }
            return (0, utils_js_1.domEach)(this, function (el, i) {
                if ((0, utils_js_1.isTag)(el))
                    setAttr(el, name, value.call(el, i, el.attribs[name]));
            });
        }
        return (0, utils_js_1.domEach)(this, function (el) {
            if (!(0, utils_js_1.isTag)(el))
                return;
            if (typeof name === 'object') {
                Object.keys(name).forEach(function (objName) {
                    var objValue = name[objName];
                    setAttr(el, objName, objValue);
                });
            }
            else {
                setAttr(el, name, value);
            }
        });
    }
    return arguments.length > 1
        ? this
        : getAttr(this[0], name, this.options.xmlMode);
}
exports.attr = attr;
/**
 * Gets a node's prop.
 *
 * @private
 * @category Attributes
 * @param el - Element to get the prop of.
 * @param name - Name of the prop.
 * @returns The prop's value.
 */
function getProp(el, name, xmlMode) {
    return name in el
        ? // @ts-expect-error TS doesn't like us accessing the value directly here.
            el[name]
        : !xmlMode && rboolean.test(name)
            ? getAttr(el, name, false) !== undefined
            : getAttr(el, name, xmlMode);
}
/**
 * Sets the value of a prop.
 *
 * @private
 * @param el - The element to set the prop on.
 * @param name - The prop's name.
 * @param value - The prop's value.
 */
function setProp(el, name, value, xmlMode) {
    if (name in el) {
        // @ts-expect-error Overriding value
        el[name] = value;
    }
    else {
        setAttr(el, name, !xmlMode && rboolean.test(name) ? (value ? '' : null) : "".concat(value));
    }
}
function prop(name, value) {
    var _this = this;
    var _a;
    if (typeof name === 'string' && value === undefined) {
        var el = this[0];
        if (!el || !(0, utils_js_1.isTag)(el))
            return undefined;
        switch (name) {
            case 'style': {
                var property_1 = this.css();
                var keys = Object.keys(property_1);
                keys.forEach(function (p, i) {
                    property_1[i] = p;
                });
                property_1.length = keys.length;
                return property_1;
            }
            case 'tagName':
            case 'nodeName': {
                return el.name.toUpperCase();
            }
            case 'href':
            case 'src': {
                var prop_1 = (_a = el.attribs) === null || _a === void 0 ? void 0 : _a[name];
                /* eslint-disable node/no-unsupported-features/node-builtins */
                if (typeof URL !== 'undefined' &&
                    ((name === 'href' && (el.tagName === 'a' || el.name === 'link')) ||
                        (name === 'src' &&
                            (el.tagName === 'img' ||
                                el.tagName === 'iframe' ||
                                el.tagName === 'audio' ||
                                el.tagName === 'video' ||
                                el.tagName === 'source'))) &&
                    prop_1 !== undefined &&
                    this.options.baseURI) {
                    return new URL(prop_1, this.options.baseURI).href;
                }
                /* eslint-enable node/no-unsupported-features/node-builtins */
                return prop_1;
            }
            case 'innerText': {
                return (0, domutils_1.innerText)(el);
            }
            case 'textContent': {
                return (0, domutils_1.textContent)(el);
            }
            case 'outerHTML':
                return this.clone().wrap('<container />').parent().html();
            case 'innerHTML':
                return this.html();
            default:
                return getProp(el, name, this.options.xmlMode);
        }
    }
    if (typeof name === 'object' || value !== undefined) {
        if (typeof value === 'function') {
            if (typeof name === 'object') {
                throw new Error('Bad combination of arguments.');
            }
            return (0, utils_js_1.domEach)(this, function (el, i) {
                if ((0, utils_js_1.isTag)(el)) {
                    setProp(el, name, value.call(el, i, getProp(el, name, _this.options.xmlMode)), _this.options.xmlMode);
                }
            });
        }
        return (0, utils_js_1.domEach)(this, function (el) {
            if (!(0, utils_js_1.isTag)(el))
                return;
            if (typeof name === 'object') {
                Object.keys(name).forEach(function (key) {
                    var val = name[key];
                    setProp(el, key, val, _this.options.xmlMode);
                });
            }
            else {
                setProp(el, name, value, _this.options.xmlMode);
            }
        });
    }
    return undefined;
}
exports.prop = prop;
/**
 * Sets the value of a data attribute.
 *
 * @private
 * @param el - The element to set the data attribute on.
 * @param name - The data attribute's name.
 * @param value - The data attribute's value.
 */
function setData(el, name, value) {
    var _a;
    var elem = el;
    (_a = elem.data) !== null && _a !== void 0 ? _a : (elem.data = {});
    if (typeof name === 'object')
        Object.assign(elem.data, name);
    else if (typeof name === 'string' && value !== undefined) {
        elem.data[name] = value;
    }
}
/**
 * Read the specified attribute from the equivalent HTML5 `data-*` attribute,
 * and (if present) cache the value in the node's internal data store. If no
 * attribute name is specified, read _all_ HTML5 `data-*` attributes in this manner.
 *
 * @private
 * @category Attributes
 * @param el - Element to get the data attribute of.
 * @param name - Name of the data attribute.
 * @returns The data attribute's value, or a map with all of the data attributes.
 */
function readData(el, name) {
    var domNames;
    var jsNames;
    var value;
    if (name == null) {
        domNames = Object.keys(el.attribs).filter(function (attrName) {
            return attrName.startsWith(dataAttrPrefix);
        });
        jsNames = domNames.map(function (domName) {
            return (0, utils_js_1.camelCase)(domName.slice(dataAttrPrefix.length));
        });
    }
    else {
        domNames = [dataAttrPrefix + (0, utils_js_1.cssCase)(name)];
        jsNames = [name];
    }
    for (var idx = 0; idx < domNames.length; ++idx) {
        var domName = domNames[idx];
        var jsName = jsNames[idx];
        if (hasOwn.call(el.attribs, domName) &&
            !hasOwn.call(el.data, jsName)) {
            value = el.attribs[domName];
            if (hasOwn.call(primitives, value)) {
                value = primitives[value];
            }
            else if (value === String(Number(value))) {
                value = Number(value);
            }
            else if (rbrace.test(value)) {
                try {
                    value = JSON.parse(value);
                }
                catch (e) {
                    /* Ignore */
                }
            }
            el.data[jsName] = value;
        }
    }
    return name == null ? el.data : value;
}
function data(name, value) {
    var _a;
    var elem = this[0];
    if (!elem || !(0, utils_js_1.isTag)(elem))
        return;
    var dataEl = elem;
    (_a = dataEl.data) !== null && _a !== void 0 ? _a : (dataEl.data = {});
    // Return the entire data object if no data specified
    if (!name) {
        return readData(dataEl);
    }
    // Set the value (with attr map support)
    if (typeof name === 'object' || value !== undefined) {
        (0, utils_js_1.domEach)(this, function (el) {
            if ((0, utils_js_1.isTag)(el)) {
                if (typeof name === 'object')
                    setData(el, name);
                else
                    setData(el, name, value);
            }
        });
        return this;
    }
    if (hasOwn.call(dataEl.data, name)) {
        return dataEl.data[name];
    }
    return readData(dataEl, name);
}
exports.data = data;
function val(value) {
    var querying = arguments.length === 0;
    var element = this[0];
    if (!element || !(0, utils_js_1.isTag)(element))
        return querying ? undefined : this;
    switch (element.name) {
        case 'textarea':
            return this.text(value);
        case 'select': {
            var option = this.find('option:selected');
            if (!querying) {
                if (this.attr('multiple') == null && typeof value === 'object') {
                    return this;
                }
                this.find('option').removeAttr('selected');
                var values = typeof value !== 'object' ? [value] : value;
                for (var i = 0; i < values.length; i++) {
                    this.find("option[value=\"".concat(values[i], "\"]")).attr('selected', '');
                }
                return this;
            }
            return this.attr('multiple')
                ? option.toArray().map(function (el) { return (0, static_js_1.text)(el.children); })
                : option.attr('value');
        }
        case 'input':
        case 'option':
            return querying
                ? this.attr('value')
                : this.attr('value', value);
    }
    return undefined;
}
exports.val = val;
/**
 * Remove an attribute.
 *
 * @private
 * @param elem - Node to remove attribute from.
 * @param name - Name of the attribute to remove.
 */
function removeAttribute(elem, name) {
    if (!elem.attribs || !hasOwn.call(elem.attribs, name))
        return;
    delete elem.attribs[name];
}
/**
 * Splits a space-separated list of names to individual names.
 *
 * @category Attributes
 * @param names - Names to split.
 * @returns - Split names.
 */
function splitNames(names) {
    return names ? names.trim().split(rspace) : [];
}
/**
 * Method for removing attributes by `name`.
 *
 * @category Attributes
 * @example
 *
 * ```js
 * $('.pear').removeAttr('class').html();
 * //=> <li>Pear</li>
 *
 * $('.apple').attr('id', 'favorite');
 * $('.apple').removeAttr('id class').html();
 * //=> <li>Apple</li>
 * ```
 *
 * @param name - Name of the attribute.
 * @returns The instance itself.
 * @see {@link https://api.jquery.com/removeAttr/}
 */
function removeAttr(name) {
    var attrNames = splitNames(name);
    var _loop_1 = function (i) {
        (0, utils_js_1.domEach)(this_1, function (elem) {
            if ((0, utils_js_1.isTag)(elem))
                removeAttribute(elem, attrNames[i]);
        });
    };
    var this_1 = this;
    for (var i = 0; i < attrNames.length; i++) {
        _loop_1(i);
    }
    return this;
}
exports.removeAttr = removeAttr;
/**
 * Check to see if _any_ of the matched elements have the given `className`.
 *
 * @category Attributes
 * @example
 *
 * ```js
 * $('.pear').hasClass('pear');
 * //=> true
 *
 * $('apple').hasClass('fruit');
 * //=> false
 *
 * $('li').hasClass('pear');
 * //=> true
 * ```
 *
 * @param className - Name of the class.
 * @returns Indicates if an element has the given `className`.
 * @see {@link https://api.jquery.com/hasClass/}
 */
function hasClass(className) {
    return this.toArray().some(function (elem) {
        var clazz = (0, utils_js_1.isTag)(elem) && elem.attribs['class'];
        var idx = -1;
        if (clazz && className.length) {
            while ((idx = clazz.indexOf(className, idx + 1)) > -1) {
                var end = idx + className.length;
                if ((idx === 0 || rspace.test(clazz[idx - 1])) &&
                    (end === clazz.length || rspace.test(clazz[end]))) {
                    return true;
                }
            }
        }
        return false;
    });
}
exports.hasClass = hasClass;
/**
 * Adds class(es) to all of the matched elements. Also accepts a `function`.
 *
 * @category Attributes
 * @example
 *
 * ```js
 * $('.pear').addClass('fruit').html();
 * //=> <li class="pear fruit">Pear</li>
 *
 * $('.apple').addClass('fruit red').html();
 * //=> <li class="apple fruit red">Apple</li>
 * ```
 *
 * @param value - Name of new class.
 * @returns The instance itself.
 * @see {@link https://api.jquery.com/addClass/}
 */
function addClass(value) {
    // Support functions
    if (typeof value === 'function') {
        return (0, utils_js_1.domEach)(this, function (el, i) {
            if ((0, utils_js_1.isTag)(el)) {
                var className = el.attribs['class'] || '';
                addClass.call([el], value.call(el, i, className));
            }
        });
    }
    // Return if no value or not a string or function
    if (!value || typeof value !== 'string')
        return this;
    var classNames = value.split(rspace);
    var numElements = this.length;
    for (var i = 0; i < numElements; i++) {
        var el = this[i];
        // If selected element isn't a tag, move on
        if (!(0, utils_js_1.isTag)(el))
            continue;
        // If we don't already have classes — always set xmlMode to false here, as it doesn't matter for classes
        var className = getAttr(el, 'class', false);
        if (!className) {
            setAttr(el, 'class', classNames.join(' ').trim());
        }
        else {
            var setClass = " ".concat(className, " ");
            // Check if class already exists
            for (var j = 0; j < classNames.length; j++) {
                var appendClass = "".concat(classNames[j], " ");
                if (!setClass.includes(" ".concat(appendClass)))
                    setClass += appendClass;
            }
            setAttr(el, 'class', setClass.trim());
        }
    }
    return this;
}
exports.addClass = addClass;
/**
 * Removes one or more space-separated classes from the selected elements. If no
 * `className` is defined, all classes will be removed. Also accepts a `function`.
 *
 * @category Attributes
 * @example
 *
 * ```js
 * $('.pear').removeClass('pear').html();
 * //=> <li class="">Pear</li>
 *
 * $('.apple').addClass('red').removeClass().html();
 * //=> <li class="">Apple</li>
 * ```
 *
 * @param name - Name of the class. If not specified, removes all elements.
 * @returns The instance itself.
 * @see {@link https://api.jquery.com/removeClass/}
 */
function removeClass(name) {
    // Handle if value is a function
    if (typeof name === 'function') {
        return (0, utils_js_1.domEach)(this, function (el, i) {
            if ((0, utils_js_1.isTag)(el)) {
                removeClass.call([el], name.call(el, i, el.attribs['class'] || ''));
            }
        });
    }
    var classes = splitNames(name);
    var numClasses = classes.length;
    var removeAll = arguments.length === 0;
    return (0, utils_js_1.domEach)(this, function (el) {
        if (!(0, utils_js_1.isTag)(el))
            return;
        if (removeAll) {
            // Short circuit the remove all case as this is the nice one
            el.attribs['class'] = '';
        }
        else {
            var elClasses = splitNames(el.attribs['class']);
            var changed = false;
            for (var j = 0; j < numClasses; j++) {
                var index = elClasses.indexOf(classes[j]);
                if (index >= 0) {
                    elClasses.splice(index, 1);
                    changed = true;
                    /*
                     * We have to do another pass to ensure that there are not duplicate
                     * classes listed
                     */
                    j--;
                }
            }
            if (changed) {
                el.attribs['class'] = elClasses.join(' ');
            }
        }
    });
}
exports.removeClass = removeClass;
/**
 * Add or remove class(es) from the matched elements, depending on either the
 * class's presence or the value of the switch argument. Also accepts a `function`.
 *
 * @category Attributes
 * @example
 *
 * ```js
 * $('.apple.green').toggleClass('fruit green red').html();
 * //=> <li class="apple fruit red">Apple</li>
 *
 * $('.apple.green').toggleClass('fruit green red', true).html();
 * //=> <li class="apple green fruit red">Apple</li>
 * ```
 *
 * @param value - Name of the class. Can also be a function.
 * @param stateVal - If specified the state of the class.
 * @returns The instance itself.
 * @see {@link https://api.jquery.com/toggleClass/}
 */
function toggleClass(value, stateVal) {
    // Support functions
    if (typeof value === 'function') {
        return (0, utils_js_1.domEach)(this, function (el, i) {
            if ((0, utils_js_1.isTag)(el)) {
                toggleClass.call([el], value.call(el, i, el.attribs['class'] || '', stateVal), stateVal);
            }
        });
    }
    // Return if no value or not a string or function
    if (!value || typeof value !== 'string')
        return this;
    var classNames = value.split(rspace);
    var numClasses = classNames.length;
    var state = typeof stateVal === 'boolean' ? (stateVal ? 1 : -1) : 0;
    var numElements = this.length;
    for (var i = 0; i < numElements; i++) {
        var el = this[i];
        // If selected element isn't a tag, move on
        if (!(0, utils_js_1.isTag)(el))
            continue;
        var elementClasses = splitNames(el.attribs['class']);
        // Check if class already exists
        for (var j = 0; j < numClasses; j++) {
            // Check if the class name is currently defined
            var index = elementClasses.indexOf(classNames[j]);
            // Add if stateValue === true or we are toggling and there is no value
            if (state >= 0 && index < 0) {
                elementClasses.push(classNames[j]);
            }
            else if (state <= 0 && index >= 0) {
                // Otherwise remove but only if the item exists
                elementClasses.splice(index, 1);
            }
        }
        el.attribs['class'] = elementClasses.join(' ');
    }
    return this;
}
exports.toggleClass = toggleClass;
//# sourceMappingURL=attributes.js.map