| 2002 | 2013 | })); |
| 2003 | 2014 | } |
| 2004 | 2015 | |
| 2016 | +/** |
| 2017 | + * Function to apply character tags to message divs when rendering the chat |
| 2018 | + * @param {object} options Options for applying character tags |
| 2019 | + * @param {number|number[]} [options.mesIds=[]] An id or array of message IDs to filter by. |
| 2020 | + * If empty, all messages will be processed. |
| 2021 | + * @returns {void} |
| 2022 | + * @description This function iterates through the chat messages and applies character tags |
| 2023 | + */ |
| 2024 | +export function applyCharacterTagsToMessageDivs({ mesIds = [] } = {}) { |
| 2025 | + try { |
| 2026 | + const messagesFilter = buildMessagesFilter(mesIds); |
| 2027 | + const messages = $('#chat').children(messagesFilter); |
| 2028 | + |
| 2029 | + // Clear existing tags |
| 2030 | + messages.each(function () { |
| 2031 | + const element = this; // Get the raw DOM element |
| 2032 | + |
| 2033 | + for (const attr of [...element.attributes]) { |
| 2034 | + if (attr.name.startsWith('data-char-tag-') || attr.name === 'data-char-tags') { |
| 2035 | + element.removeAttribute(attr.name); |
| 2036 | + } |
| 2037 | + } |
| 2038 | + }); |
| 2039 | + |
| 2040 | + const tagsList = tags, characterTagData = tag_map; |
| 2041 | + |
| 2042 | + if (!tagsList?.length || !characterTagData) { |
| 2043 | + return; |
| 2044 | + } |
| 2045 | + |
| 2046 | + const tagNamesById = tagsList.reduce((acc, tag) => { |
| 2047 | + acc[tag.id] = tag.name; |
| 2048 | + return acc; |
| 2049 | + }, {}); |
| 2050 | + |
| 2051 | + const characterTagsCache = new Map(); |
| 2052 | + |
| 2053 | + // Iterate each message div |
| 2054 | + messages.each(function () { |
| 2055 | + const $this = $(this); // Store the jQuery object |
| 2056 | + const avatarFileName = extractCharacterAvatar($this.find('.avatar img').attr('src')); |
| 2057 | + |
| 2058 | + if (!avatarFileName) { |
| 2059 | + return; |
| 2060 | + } |
| 2061 | + |
| 2062 | + let tagsForCharacter = characterTagsCache.get(avatarFileName); |
| 2063 | + |
| 2064 | + // If tags are NOT in the cache, compute and store them |
| 2065 | + if (!tagsForCharacter) { |
| 2066 | + const tagIds = characterTagData[avatarFileName]; |
| 2067 | + if (tagIds?.length) { |
| 2068 | + const tagNames = tagIds |
| 2069 | + .map(id => tagNamesById[id]) |
| 2070 | + .filter(Boolean); |
| 2071 | + |
| 2072 | + if (tagNames.length) { |
| 2073 | + tagsForCharacter = { |
| 2074 | + tagNames, |
| 2075 | + joinedTagNames: tagNames |
| 2076 | + .map(name => name?.replace(/,/g, ' ')) // replace commas with spaces to avoid issues with tag names containing commas |
| 2077 | + .join(','), |
| 2078 | + }; |
| 2079 | + // Add the newly computed tags to the cache |
| 2080 | + characterTagsCache.set(avatarFileName, tagsForCharacter); |
| 2081 | + } |
| 2082 | + } |
| 2083 | + } |
| 2084 | + |
| 2085 | + // If we have tags (either from cache or newly computed), apply them |
| 2086 | + if (tagsForCharacter) { |
| 2087 | + applyTags($this, tagsForCharacter); |
| 2088 | + } |
| 2089 | + }); |
| 2090 | + } catch (error) { |
| 2091 | + console.error('Error applying character tags to message divs:', error); |
| 2092 | + } |
| 2093 | +} |
| 2094 | + |
| 2095 | +/** |
| 2096 | + * Builds a jQuery selector string to filter messages by their IDs. |
| 2097 | + * @param {number|number[]} mesIds - An id or array of message IDs to filter by. |
| 2098 | + * @returns {string} A jQuery selector string that matches messages with the specified IDs. |
| 2099 | + * If mesIds is empty, it returns '.mes' to select all messages. |
| 2100 | + * @example |
| 2101 | + * buildMessagesFilter([1, 5]); // Returns '.mes[mesid="1"],.mes[mesid="5"]' |
| 2102 | + * buildMessagesFilter([]); // Returns '.mes' |
| 2103 | + */ |
| 2104 | +function buildMessagesFilter(mesIds) { |
| 2105 | + const allMessages = '.mes'; |
| 2106 | + |
| 2107 | + if (!mesIds) { |
| 2108 | + return allMessages; // If no mesIds provided, select all messages |
| 2109 | + } |
| 2110 | + |
| 2111 | + const mesIdsArray = Array.isArray(mesIds) ? mesIds : [mesIds]; |
| 2112 | + |
| 2113 | + if (mesIdsArray?.length) { |
| 2114 | + // Create a valid jQuery selector for multiple attribute values. |
| 2115 | + // Example output: '.mes[mesid="1"],.mes[mesid="5"]' |
| 2116 | + return mesIdsArray.map(id => `.mes[mesid="${id}"]`).join(','); |
| 2117 | + } |
| 2118 | + |
| 2119 | + // If mesIds is empty, select all messages. |
| 2120 | + return allMessages; |
| 2121 | +} |
| 2122 | + |
| 2123 | +/** |
| 2124 | + * Helper function to apply all necessary data attributes to a DOM element. |
| 2125 | + * @param {JQuery<HTMLElement>} $element - The jQuery object for the message div. |
| 2126 | + * @param {object} tagData - An object containing tag information. |
| 2127 | + * @param {string[]} tagData.tagNames - An array of tag names. |
| 2128 | + * @param {string} tagData.joinedTagNames - A comma-separated string of tag names. |
| 2129 | + */ |
| 2130 | +function applyTags($element, tagData) { |
| 2131 | + $element.attr('data-char-tags', tagData.joinedTagNames); |
| 2132 | + tagData.tagNames.forEach(tagName => { |
| 2133 | + const normalizedTagName = normalizeTagName(tagName); |
| 2134 | + |
| 2135 | + if (!normalizedTagName) { |
| 2136 | + return; // Skip empty tag names |
| 2137 | + } |
| 2138 | + |
| 2139 | + $element.attr(`data-char-tag-${normalizedTagName}`, ''); |
| 2140 | + }); |
| 2141 | +} |
| 2142 | + |
| 2143 | +/** |
| 2144 | + * Normalizes a tag name by trimming, converting spaces to hyphens, replacing accented characters, |
| 2145 | + * removing special characters, and converting to lowercase. |
| 2146 | + * @param {string} name The tag name to normalize. |
| 2147 | + * @returns {string} The normalized tag name. |
| 2148 | + */ |
| 2149 | +function normalizeTagName(name) { |
| 2150 | + if (!name?.trim()) { |
| 2151 | + return ''; |
| 2152 | + } |
| 2153 | + |
| 2154 | + // Normalize the tag name by trimming, converting spaces to hyphens, replacing accented characters, removing special characters, and converting to lowercase |
| 2155 | + return name.trim() |
| 2156 | + .normalize('NFD') // Normalize accented characters |
| 2157 | + .replace(/[\u0300-\u036f]/g, '') // Remove diacritical marks |
| 2158 | + .replace(/[^a-zA-Z0-9\s_-]/g, '') // Remove special characters except spaces, underscores, and hyphens |
| 2159 | + .replace(/[\s_]+/g, '-') // Replace spaces and underscores with hyphens |
| 2160 | + .toLowerCase(); |
| 2161 | +} |
| 2162 | + |
| 2163 | +/** Extracts the character avatar file name from the avatar source URL. |
| 2164 | + * @param {string} avatarSrc The source URL of the character avatar. |
| 2165 | + * @returns {string|null} The normalized avatar file name, or null if the input is falsy or doesn't contain a valid file name. |
| 2166 | + */ |
| 2167 | +function extractCharacterAvatar(avatarSrc) { |
| 2168 | + if (!avatarSrc) { |
| 2169 | + return null; |
| 2170 | + } |
| 2171 | + |
| 2172 | + try { |
| 2173 | + const url = new URL(avatarSrc, window.location.origin); |
| 2174 | + return url?.searchParams.get('file'); |
| 2175 | + } catch (error) { |
| 2176 | + console.error('Unable to parse character avatar using avatarSrc', avatarSrc, error); |
| 2177 | + return null; |
| 2178 | + } |
| 2179 | +} |
| 2180 | + |
| 2005 | 2181 | export function initTags() { |
| 2006 | 2182 | createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } }); |
| 2007 | 2183 | createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } }); |