// ==UserScript== // @name Gym Order Injector // @namespace https://github.com/jungheil/goInjector // @license Mulan PSL v2 // @copyright 2025 Jungheil // @version 1.2.1 // @description Inject order into Gym Booking System // @icon https://www.sysu.edu.cn/favicon.ico // @author Jungheil // @homepage https://github.com/jungheil/goInjector // @match *://gym.sysu.edu.cn/* // @grant none // @run-at document-start // @downloadURL https://raw.githubusercontent.com/jungheil/goInjector/main/goInjector.user.js // @updateURL https://raw.githubusercontent.com/jungheil/goInjector/main/goInjector.meta.js // @supportURL https://github.com/jungheil/goInjector // ==/UserScript== // Copyright (c) 2025 Jungheil // Gym Order Injector is licensed under Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: // http://license.coscl.org.cn/MulanPSL2 // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, // EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, // MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. (function () { "use strict"; let initializationAttempts = 0; const maxAttempts = 3; async function tryInitialize() { try { console.log( `goInjector 开始初始化 (尝试 ${initializationAttempts + 1 }/${maxAttempts})` ); // Original XHR open override const xhrOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function () { let venueConfig = JSON.parse(localStorage.getItem("venueConfig")); const xhr = this; let url = new URL(arguments[1]); if ( (url.pathname == "/api/BookingRequestVenue" && venueConfig?.isInitiator) || (url.pathname == "/api/BookingRequestVenue/Participants" && !venueConfig?.isInitiator) ) { const getter = Object.getOwnPropertyDescriptor( XMLHttpRequest.prototype, "responseText" ).get; Object.defineProperty(xhr, "responseText", { get: () => { let result = getter.call(xhr); const bookingInfo = localStorage.getItem("BookingInfo"); if (bookingInfo) { let ret = JSON.parse(result); ret.push(JSON.parse(bookingInfo)); return JSON.stringify(ret); } return result; }, }); } return xhrOpen.apply(xhr, arguments); }; const venueType = await getVenueType(); await generateBookingInfo(); initializeUI(venueType); console.log("goInjector 初始化完成"); return true; } catch (error) { console.error( `goInjector 初始化失败 (尝试 ${initializationAttempts + 1 }/${maxAttempts}):`, error ); return false; } } async function initializeWithRetry() { console.log("goInjector 准备初始化..."); console.log("当前页面状态:", document.readyState); while (initializationAttempts < maxAttempts) { const success = await tryInitialize(); if (success) { return; } initializationAttempts++; if (initializationAttempts < maxAttempts) { console.log( `goInjector 将在 1 秒后重试初始化 (${initializationAttempts + 1 }/${maxAttempts})...` ); await new Promise((resolve) => setTimeout(resolve, 1000)); } } console.error(`goInjector 初始化失败,已达到最大重试次数 (${maxAttempts})`); } function startInitialization() { if (localStorage.getItem("scientia-session-authorization")) { // 立即尝试初始化 initializeWithRetry(); } else { console.log("未登录,goInjector 放弃初始化"); } } // 使用标志位防止重复初始化 let hasInitialized = false; function handleInitialization() { if (!hasInitialized) { console.log("页面状态:", document.readyState); hasInitialized = true; startInitialization(); } } // 监听 readystatechange 事件 document.addEventListener("readystatechange", function handler(e) { if (document.readyState !== "loading") { document.removeEventListener("readystatechange", handler); handleInitialization(); } }); // 如果页面已经加载完成,立即开始初始化 if (document.readyState !== "loading") { handleInitialization(); } function requestVenueType() { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://gym.sysu.edu.cn/api/venuetype/all", true); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(new Error(`get venue type failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get venue type failed")); }; xhr.send(); }); } async function getVenueType() { let vt = localStorage.getItem("venueType"); if (!vt) { vt = await requestVenueType(); localStorage.setItem("venueType", vt); } return JSON.parse(vt); } function requestVenue(venueTypeId) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open( "GET", "https://gym.sysu.edu.cn/api/venue/type/" + venueTypeId, true ); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(new Error(`get venue failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get venue failed")); }; xhr.send(); }); } function requestMe() { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("GET", "https://gym.sysu.edu.cn/api/Credit/Me", true); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.responseText); } else { reject(new Error(`get me failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get me failed")); }; xhr.send(); }); } async function getMe() { var Me = localStorage.getItem("Me"); if (!Me) { Me = await requestMe(); localStorage.setItem("Me", Me); } return JSON.parse(Me); } function requestCreditFee(VenueTypeId) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open( "GET", "https://gym.sysu.edu.cn/api/venuetype/" + VenueTypeId + "/feetemplates", true ); const access_token = JSON.parse( localStorage.getItem("scientia-session-authorization") ).access_token; xhr.setRequestHeader("Authorization", "Bearer " + access_token); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { const result = JSON.parse(xhr.responseText).find( (item) => item.UserRole === "学生" ).CreditFee; resolve(result); } else { reject(new Error(`get credit fee failed, status: ${xhr.status}`)); } } }; xhr.onerror = () => { reject(new Error("get credit fee failed")); }; xhr.send(); }); } function getRandomTime(start, end) { const startDate = new Date(start); const endDate = new Date(end); const randomDate = new Date( startDate.getTime() + Math.random() * (endDate.getTime() - startDate.getTime()) ); return randomDate; } async function generateBookingInfo() { const venueConfig = JSON.parse(localStorage.getItem("venueConfig")); const Me = await getMe(); if (venueConfig && venueConfig.VenueTypeId && venueConfig.VenueId) { try { const VenueTypeInfo = (await getVenueType()).find( (item) => item.Identity === venueConfig.VenueTypeId ); const VenueInfo = JSON.parse( await requestVenue(venueConfig.VenueTypeId) ).find((item) => item.Identity === venueConfig.VenueId); let startDateTime, endDateTime; if (venueConfig.timeSlot && venueConfig.bookingDate) { let timeSlot = JSON.parse(venueConfig.timeSlot); startDateTime = new Date( venueConfig.bookingDate + "T" + timeSlot.startTime.split("T")[1] ); endDateTime = new Date( venueConfig.bookingDate + "T" + timeSlot.endTime.split("T")[1] ); } else { let now = new Date(); const timeSlots = VenueInfo.Schedules[0].TimeSlots; const currentTime = now.getHours() + now.getMinutes() / 60; const bookingTime = currentTime + 0.25; let targetSlot; for (let slot of timeSlots) { const slotStartTime = parseInt(slot.End.split(":")[0]) + parseInt(slot.End.split(":")[1]) / 60; if (slotStartTime >= bookingTime) { targetSlot = slot; break; } } if (!targetSlot) { targetSlot = timeSlots[0]; now.setDate(now.getDate() + 1); } if (bookingTime < parseInt(timeSlots[0].Start.split(":")[0])) { targetSlot = timeSlots[0]; } startDateTime = new Date( now.getFullYear(), now.getMonth(), now.getDate(), parseInt(targetSlot.Start.split(":")[0]), parseInt(targetSlot.Start.split(":")[1]) ); endDateTime = new Date( now.getFullYear(), now.getMonth(), now.getDate(), parseInt(targetSlot.End.split(":")[0]), parseInt(targetSlot.End.split(":")[1]) ); } const CreateAt = getRandomTime( startDateTime.getTime() - 2 * 24 * 60 * 60 * 1000, startDateTime.getTime() ); const UpdateAt = getRandomTime(CreateAt, endDateTime); if (venueConfig.isInitiator) { let BookingInfo = { Identity: venueConfig.Identity || crypto.randomUUID(), Name: venueConfig.Name || Me.Name, BookingId: venueConfig.bookingId || "#RB-" + Array(10) .fill() .map( () => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[ Math.floor(Math.random() * 36) ] ) .join(""), UserId: venueConfig.UserId || Me.UserId, VenueTypeId: venueConfig.VenueTypeId, VenueId: venueConfig.VenueId, VenueName: VenueInfo.Name, StartDateTime: startDateTime, EndDateTime: endDateTime, Participants: venueConfig.participant .filter((p) => p.Name && p.HostKey) .map((p) => ({ UserId: p.UserId || Array(Math.floor(4 + Math.random() * 4)) .fill() .map( () => "abcdefghijklmnopqrstuvwxyz"[ Math.floor(Math.random() * 26) ] ) .join("") + Math.floor(Math.random() * 999).toString(), Name: p.Name + " (" + p.HostKey + ")", HostKey: p.HostKey, Status: "Accepted", })), Status: "Accepted", Description: VenueTypeInfo.Name, CreatedAt: CreateAt, UpdatedAt: UpdateAt, ActionedBy: venueConfig.ActionedBy || (venueConfig.participant && venueConfig.participant[ Math.floor(Math.random() * venueConfig.participant.length) ]?.UserId) || venueConfig.UserId || Me.UserId, Charge: await requestCreditFee(venueConfig.VenueTypeId), IsCash: false, }; localStorage.setItem("BookingInfo", JSON.stringify(BookingInfo)); } else if (!venueConfig.isInitiator && venueConfig.Name) { let BookingInfo = { Identity: venueConfig.Identity || crypto.randomUUID(), Name: venueConfig.Name, BookingId: venueConfig.BookingId || "#RB-" + Array(10) .fill() .map( () => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[ Math.floor(Math.random() * 36) ] ) .join(""), UserId: venueConfig.UserId || Array(Math.floor(4 + Math.random() * 4)) .fill() .map( () => "abcdefghijklmnopqrstuvwxyz"[Math.floor(Math.random() * 26)] ) .join("") + Math.floor(Math.random() * 999).toString(), VenueTypeId: venueConfig.VenueTypeId, VenueId: venueConfig.VenueId, VenueName: VenueInfo.Name, StartDateTime: startDateTime, EndDateTime: endDateTime, Participants: venueConfig.participant.filter((p) => p.Name && p.HostKey) .length === 0 ? [ { UserId: Me.UserId, Name: Me.Name + " (" + Me.HostKey + ")", HostKey: Me.HostKey, Status: "Accepted", }, ] : venueConfig.participant .filter((p) => p.Name && p.HostKey) .map((p) => ({ UserId: p.UserId || Array(Math.floor(4 + Math.random() * 4)) .fill() .map( () => "abcdefghijklmnopqrstuvwxyz"[ Math.floor(Math.random() * 26) ] ) .join("") + Math.floor(Math.random() * 999).toString(), Name: p.Name + " (" + p.HostKey + ")", HostKey: p.HostKey, Status: "Accepted", })), Status: "Accepted", Description: VenueTypeInfo.Name, CreatedAt: CreateAt, UpdatedAt: UpdateAt, ActionedBy: venueConfig.ActionedBy || (venueConfig.participant && venueConfig.participant[ Math.floor(Math.random() * venueConfig.participant.length) ]?.UserId) || Me.UserId || venueConfig.UserId, Charge: await requestCreditFee(venueConfig.VenueTypeId), IsCash: false, }; localStorage.setItem("BookingInfo", JSON.stringify(BookingInfo)); } else { localStorage.setItem("BookingInfo", ""); } } catch (error) { console.error("生成预约信息失败:", error); localStorage.setItem("BookingInfo", ""); } } else { localStorage.setItem("BookingInfo", ""); } } function initializeUI(venueType) { let participants = []; const style = document.createElement("style"); style.textContent = ` .config-button { position: fixed; left: 20px; top: 20px; z-index: 9999; padding: 10px 20px; background: rgba(255, 255, 255, 0); color: rgba(255, 255, 255, 0.02); border: none; border-radius: 4px; cursor: pointer; transition: opacity 0.3s; } .config-button:hover { opacity: 0.8; } .config-dialog { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); z-index: 10000; max-height: 80vh; overflow-y: auto; min-width: 400px; width: 90%; max-width: 600px; } .config-dialog::-webkit-scrollbar { width: 8px; } .config-dialog::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .config-dialog::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .config-dialog::-webkit-scrollbar-thumb:hover { background: #555; } .config-dialog input { display: block; margin: 10px 0; padding: 5px; width: 300px; } .input-group { margin-bottom: 12px; } .input-label { display: block; font-size: 12px; color: #063; margin-bottom: 2px; } .config-dialog select, .config-dialog input { width: 100%; padding: 6px 12px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .button-group { display: flex; justify-content: flex-end; gap: 10px; } .save-btn { padding: 8px 20px; background: #063; color: white; border: none; border-radius: 4px; cursor: pointer; } .cancel-btn { padding: 8px 20px; background: #f5f5f5; color: #666; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } h3 { color: #063; margin-bottom: 16px; } .participants-section { margin-top: 12px; } .participant-item { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .participant-item input { flex: 1; min-width: 0; } .remove-participant { background: none; border: none; color: #ff4444; font-size: 18px; cursor: pointer; padding: 5px; line-height: 1; transition: color 0.2s; } .remove-participant:hover { color: #ff0000; } .add-participant { background: #063; color: white; border: none; border-radius: 4px; cursor: pointer; padding: 8px 16px; margin-bottom: 16px; transition: all 0.3s ease; } .add-participant:disabled { background: #cccccc; cursor: not-allowed; opacity: 0.7; } .collapsible { margin-top: 12px; } .collapse-header { display: flex; align-items: center; cursor: pointer; user-select: none; color: #063; font-size: 14px; margin-bottom: 12px; } .collapse-header:hover { opacity: 0.8; } .collapse-icon { width: 16px; height: 16px; margin-right: 8px; transition: transform 0.3s; color: #063; } .collapse-icon.expanded { transform: rotate(90deg); } .collapse-content { display: none; } .collapse-content.expanded { display: block; } `; document.head.appendChild(style); const dialog = document.createElement("div"); dialog.className = "config-dialog"; dialog.innerHTML = `

注入配置

非必填选项
`; document.body.appendChild(dialog); function showDialog() { dialog.style.display = "block"; document .getElementById("config-autoDate") .addEventListener("change", function (e) { const manualDateGroup = document.getElementById("manualDateGroup"); manualDateGroup.style.display = e.target.value === "0" ? "block" : "none"; }); const savedConfig = JSON.parse(localStorage.getItem("venueConfig")) || { isInitiator: true, VenueTypeId: "", VenueId: "", timeSlot: "", bookingDate: "", participant: [], Name: "", UserId: "", Identity: "", bookingId: "", createdAt: "", updatedAt: "", ActionedBy: "", }; document.getElementById("config-isInitiator").checked = savedConfig.isInitiator; if (!savedConfig.isInitiator) { document .getElementById("config-isInitiator") .dispatchEvent(new Event("change")); } setTimeout(() => { document.getElementById("config-VenueTypeId").value = savedConfig.VenueTypeId; document .getElementById("config-VenueTypeId") .dispatchEvent(new Event("change")); setTimeout(() => { document.getElementById("config-VenueId").value = savedConfig.VenueId; document .getElementById("config-VenueId") .dispatchEvent(new Event("change")); setTimeout(() => { document.getElementById("config-timeSlot").value = savedConfig.timeSlot; }, 100); }, 100); }, 100); document.getElementById("config-bookingDate").value = savedConfig.bookingDate; if (savedConfig.bookingDate && savedConfig.timeSlot) { document.getElementById("config-autoDate").value = 0; document .getElementById("config-autoDate") .dispatchEvent(new Event("change")); } if (savedConfig.participant) { participants = savedConfig.participant; renderparticipants(); } document.getElementById("config-Name").value = savedConfig.Name; document.getElementById("config-UserId").value = savedConfig.UserId; document.getElementById("config-Identity").value = savedConfig.Identity; document.getElementById("config-bookingId").value = savedConfig.bookingId; document.getElementById("config-createdAt").value = savedConfig.createdAt; document.getElementById("config-updatedAt").value = savedConfig.updatedAt; document.getElementById("config-ActionedBy").value = savedConfig.ActionedBy; } const floatButton = document.createElement("button"); floatButton.className = "config-button"; floatButton.textContent = "Inject!"; let pressTimer; let isLongPress = false; floatButton.addEventListener("mousedown", () => { pressTimer = setTimeout(() => { isLongPress = true; }, 800); }); floatButton.addEventListener("mouseup", () => { clearTimeout(pressTimer); if (isLongPress) { showDialog(); } isLongPress = false; }); floatButton.addEventListener("mouseleave", () => { clearTimeout(pressTimer); isLongPress = false; }); floatButton.addEventListener("touchstart", (e) => { e.preventDefault(); pressTimer = setTimeout(() => { isLongPress = true; }, 800); }); floatButton.addEventListener("touchend", (e) => { e.preventDefault(); clearTimeout(pressTimer); if (isLongPress) { showDialog(); } isLongPress = false; }); floatButton.addEventListener("touchcancel", (e) => { e.preventDefault(); clearTimeout(pressTimer); isLongPress = false; }); document.body.appendChild(floatButton); document .getElementById("saveConfig") .addEventListener("click", async () => { const savedConfig = { isInitiator: document.getElementById("config-isInitiator").checked, VenueTypeId: document.getElementById("config-VenueTypeId").value, VenueId: document.getElementById("config-VenueId").value, timeSlot: document.getElementById("config-timeSlot").value, bookingDate: document.getElementById("config-bookingDate").value, participant: participants, Name: document.getElementById("config-Name").value, UserId: document.getElementById("config-UserId").value, Identity: document.getElementById("config-Identity").value, bookingId: document.getElementById("config-bookingId").value, createdAt: document.getElementById("config-createdAt").value, updatedAt: document.getElementById("config-updatedAt").value, ActionedBy: document.getElementById("config-ActionedBy").value, }; if (document.getElementById("config-autoDate").value === "1") { savedConfig.bookingDate = ""; savedConfig.timeSlot = ""; } localStorage.setItem("venueConfig", JSON.stringify(savedConfig)); await generateBookingInfo(); dialog.style.display = "none"; window.location.reload(); }); document.getElementById("cancelConfig").addEventListener("click", () => { dialog.style.display = "none"; }); document .getElementById("config-isInitiator") .addEventListener("change", function (e) { if (e.target.checked) { const isInitiatorFields = document.getElementsByClassName( "is-initiator-fields" ); for (let field of isInitiatorFields) { field.style.display = "block"; field.querySelectorAll("input").forEach((input) => { input.id = input.id.replace("-disabled", ""); }); field.querySelectorAll("div").forEach((button) => { button.id = button.id.replace("-disabled", ""); }); } const notInitiatorFields = document.getElementsByClassName( "not-initiator-fields" ); for (let field of notInitiatorFields) { field.style.display = "none"; field.querySelectorAll("input").forEach((input) => { input.id = input.id + "-disabled"; }); field.querySelectorAll("div").forEach((button) => { button.id = button.id + "-disabled"; }); } } else { const isInitiatorFields = document.getElementsByClassName( "is-initiator-fields" ); for (let field of isInitiatorFields) { field.style.display = "none"; field.querySelectorAll("input").forEach((input) => { input.id = input.id + "-disabled"; }); field.querySelectorAll("div").forEach((button) => { button.id = button.id + "-disabled"; }); } const notInitiatorFields = document.getElementsByClassName( "not-initiator-fields" ); for (let field of notInitiatorFields) { field.style.display = "block"; field.querySelectorAll("input").forEach((input) => { input.id = input.id.replace("-disabled", ""); }); field.querySelectorAll("div").forEach((button) => { button.id = button.id.replace("-disabled", ""); }); } } document.getElementById("config-Name").value = document.getElementById("config-Name-disabled").value || ""; }); document .getElementById("config-VenueTypeId") .addEventListener("change", function (e) { const venueSelect = document.getElementById("config-VenueId"); const timeSlotSelect = document.getElementById("config-timeSlot"); if (!e.target.value) { venueSelect.disabled = true; venueSelect.innerHTML = ''; timeSlotSelect.disabled = true; timeSlotSelect.innerHTML = ''; return; } requestVenue(e.target.value) .then((res) => { venueSelect.innerHTML = ` ${JSON.parse(res) .map( (venue) => ` ` ) .join("")} `; venueSelect.disabled = false; }) .catch((error) => { console.error("获取场地失败:", error); venueSelect.disabled = true; venueSelect.innerHTML = ''; }); if ( venueType.find((res) => res.Identity === e.target.value) .BookingType == "ResourcePool" ) { document.getElementById("addParticipant").disabled = true; document.getElementById("config-isInitiator").disabled = true; document.getElementById("config-isInitiator").checked = true; document .getElementById("config-isInitiator") .dispatchEvent(new Event("change")); participants = []; renderparticipants(); } else { document.getElementById("addParticipant").disabled = false; document.getElementById("config-isInitiator").disabled = false; } }); document .getElementById("config-VenueId") .addEventListener("change", function (e) { const timeSlotSelect = document.getElementById("config-timeSlot"); if (!e.target.value) { timeSlotSelect.disabled = true; timeSlotSelect.innerHTML = ''; return; } const selectedOption = e.target.selectedOptions[0]; const schedules = JSON.parse(selectedOption.dataset.schedules); const today = new Date(); const todayStr = today.toISOString().split("T")[0]; const validSchedule = schedules.find( (schedule) => schedule.StartDate <= todayStr && schedule.EndDate >= todayStr ); if (validSchedule) { timeSlotSelect.innerHTML = ` ${validSchedule.TimeSlots.map((slot) => { const startTime = new Date(`${todayStr}T${slot.Start}:00`); const endTime = new Date(`${todayStr}T${slot.End}:00`); return ` `; }).join("")} `; timeSlotSelect.disabled = false; } }); function createParticipantItem(participant, index) { const div = document.createElement("div"); div.className = "participant-item"; div.innerHTML = ` `; return div; } function renderparticipants() { const container = document.getElementById("participantsList"); container.innerHTML = ""; participants.forEach((participant, index) => { container.appendChild(createParticipantItem(participant, index)); }); } window.updateParticipant = function (index, field, value) { participants[index][field] = value; }; window.removeParticipant = function (index) { participants.splice(index, 1); renderparticipants(); }; document.getElementById("addParticipant").addEventListener("click", () => { participants.push({ Name: "", HostKey: "", UserId: "" }); renderparticipants(); }); document .getElementById("optionalFieldsToggle") .addEventListener("click", function () { const content = document.getElementById("optionalFields"); const icon = this.querySelector(".collapse-icon"); content.classList.toggle("expanded"); icon.classList.toggle("expanded"); }); } })();