diff --git a/.vscode/settings.json b/.vscode/settings.json index 0b35d26..5c257be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,8 @@ "dmail", "dmails", "dnpcache", + "downvotes", + "downvoting", "Dragonfruit", "dtext", "Earlopain", @@ -55,6 +57,7 @@ "onshift", "paysite", "Pixiv", + "postface", "postid", "postinghacks", "postingunreleasedcontent", diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 5ca9d13..57ef74a 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -13,7 +13,7 @@ // "description": "Log output to console" // } "New Component File": { - "description": "A new re621.ModTools component", + "description": "A new Mod Tools Component", "isFileTemplate": true, "prefix": ["component"], "body": [ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c301d68 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +A collection of utilities for using and moderating the booru site e621. + +## Downstream projects +Take a careful look at `package.json`, `bin/userscript-header.js`, & `src/js/models/data/Script.ts`; these 3 points are the main source of project-specific configuration. If done properly, modifying these 3 files for your project's needs should minimize merge conflicts with this project. \ No newline at end of file diff --git a/bin/userscript-header.js b/bin/userscript-header.js index 0eca786..08f14ce 100644 --- a/bin/userscript-header.js +++ b/bin/userscript-header.js @@ -1,4 +1,69 @@ const pkg = require('../package.json'); +const pkgLock = (() => { + try { + return require('../package-lock.json'); + } catch (error) { + return undefined; + } +})(); +const fs = require('node:fs'); +const bunLock = (() => { + try { + const contents = fs.readFileSync("../bun.lock", { encoding: "utf-8" }); + return JSON.parse(contents.replace(/,$(?=\s+[}\]])/gm, "")); + } catch (error) { + return undefined; + } +})(); +// It's an indirect dependency, but it's not required. +const chalk = (() => { + try { + return require("chalk"); + } catch (error) { + return undefined; + } +})(); +function tryChalk(funcName, text) { + return chalk?.[funcName]?.(text) ?? text; +} + +const fallbacks = { + "jquery": "3.6.0", + "jquery-ui": "1.12.1", + "@re621/zestyapi": "latest", + "jquery.hotkeys": "0.2.0", +} +/** + * Attempts to retrieve the version of the dependency directly from the package + * manager's lockfile. + * + * Ensures the dependency referenced by the script is exactly the same as the + * one used in development; prior to this, both the `jquery-ui` & + * `jquery.hotkeys` dependencies were actually referencing a newer & older + * version (respectively) in the development environment than the version + * requested by the userscript with the previously static `require` strings. + * + * Attempts to pull from both an NPM `package-lock.json` file and a Bun + * `bun.lock` file to make a potential future package manager change smoother. + * @param {string} key + * @returns {string} + */ +function getDependencyVersion(key) { + console.info("Attempting to retrieve version info for %s; starting with 'package-lock.json'...", key); + const fromPkgLock = pkgLock?.packages?.[`node_modules/${key}`]?.version; + const fromBunLock = bunLock?.packages?.[key]?.[0]?.match(new RegExp(`^${RegExp.escape(key)}@([0-9.]+)$`))?.[1]; + if (fromPkgLock) { + if (fromBunLock && fromPkgLock !== fromBunLock) console.warn(tryChalk("yellowBright", "[WARNING]") + ": version from 'package-lock.json' (%s) & 'bun.lock' (%s) are not the same; if you're using Bun for package installation, be sure to update 'package-lock.json' with NPM before submitting a PR.", fromPkgLock, fromBunLock); + return fromPkgLock; + } + console.warn("Failed to find version from 'package-lock.json'..."); + + if (fromBunLock) return fromBunLock; + console.warn("Failed to find version from 'bun.lock'..."); + const fromFallbacks = fallbacks[key]; + if (fromFallbacks) return fromFallbacks; + throw new Error(`Failed to find version for ${key}`); +} module.exports = { name: pkg.displayName, @@ -10,22 +75,19 @@ module.exports = { homepageURL: pkg.homepage, supportURL: pkg.homepage + "/issues", - icon: "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", - icon64: "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", + icon: pkg.config.iconUri ?? pkg.config.icon64Uri, + icon64: pkg.config.icon64Uri ?? pkg.config.iconUri, updateURL: pkg.homepage + "/releases/latest/download/script.meta.js", downloadURL: pkg.homepage + "/releases/latest/download/script.user.js", - match: [ - "https://e621.net/*", - "https://e926.net/*", - ], + match: pkg.config.domains.map(e => `https://${e}/*`), require: [ - "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js", - "https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js", - "https://cdn.jsdelivr.net/npm/@re621/zestyapi@latest/dist/ZestyAPI.min.js", - "https://cdnjs.cloudflare.com/ajax/libs/jquery.hotkeys/0.2.0/jquery.hotkeys.min.js", + `https://cdnjs.cloudflare.com/ajax/libs/jquery/${getDependencyVersion("jquery")}/jquery.min.js`, + `https://cdnjs.cloudflare.com/ajax/libs/jqueryui/${getDependencyVersion("jquery-ui")}/jquery-ui.min.js`, + `https://cdn.jsdelivr.net/npm/@re621/zestyapi@${getDependencyVersion("@re621/zestyapi")}/dist/ZestyAPI.min.js`, + `https://cdnjs.cloudflare.com/ajax/libs/jquery.hotkeys/${getDependencyVersion("jquery.hotkeys")}/jquery.hotkeys.min.js`, ], grant: [ @@ -44,7 +106,7 @@ module.exports = { ], connect: [ - "static1.e621.net", + ...(pkg.config.staticDomains ?? pkg.config.domains.map(e => `static1.${e}`)), "*", ], diff --git a/package-lock.json b/package-lock.json index faac112..faadf38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mod621", - "version": "2.1.1", + "version": "6.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mod621", - "version": "2.1.1", + "version": "6.5.0", "license": "GPL-3.0", "dependencies": { "@re621/zestyapi": "^0.1.13", diff --git a/package.json b/package.json index 7283cf8..b5371ce 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "mod621", "displayName": "RE621 Mod Tools", "namespace": "mod621.bitwolfy.com", - "version": "6.4.3", + "version": "6.5.0", "description": "Moderation toolkit for e621.net", "author": "bitWolfy", "contributors": [ @@ -14,6 +14,12 @@ "homepage": "https://github.com/re621/re621.ModTools", "funding": "https://ko-fi.com/bitwolfy", "repository": "github:re621/re621.ModTools", + "config": { + "iconUri": "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", + "icon64Uri": "https://cdn.jsdelivr.net/gh/re621/re621@master/assets/icon64.png", + "domains": ["e621.net", "e926.net"], + "staticDomains": ["static1.e621.net", "static1.e926.net"] + }, "scripts": { "push": "npm version patch && git push --follow-tags", "push-minor": "npm version minor && git push --follow-tags", diff --git a/src/REMT.ts b/src/REMT.ts index 56b94fc..fd4e175 100644 --- a/src/REMT.ts +++ b/src/REMT.ts @@ -53,12 +53,12 @@ export default class REMT { if (Page.matches(IgnoredPages)) return; - console.log("%c[RE621.ModTools]%c v." + Script.version, "color: maroon", "color: unset"); + Debug.logPrefix("v." + Script.versionObj.full); // Set up the API connection // TODO: Temporary instantiation method REMT.API = window["ZestyAPI"].connect({ - userAgent: Script.userAgent, + userAgent: Util.Network.userAgent, debug: Debug.Connect, }); @@ -77,7 +77,7 @@ export default class REMT { bodyLoaded = PageObserver.watch("body").then(() => { Debug.log("+ BODY is ready"); - $("body").attr("remt", Script.version); + $("body").attr(Script.htmlPrefix, Script.versionObj.full); Util.DOM.setupDialogContainer(); // TODO Move to the dialog class User.init(); }); @@ -105,16 +105,16 @@ export default class REMT { REMT._Registry[instance.getName()] = instance; await instance.bootstrapSettings(); } - Util.Events.trigger("re621-mt:bootstrap"); + Util.Events.trigger(`${Script.eventPrefixLegacy}:bootstrap`); // Load modules (asynchronous) const promises: Promise[] = []; for (const instance of Object.values(REMT._Registry)) promises.push(instance?.load() ?? Promise.resolve()); Promise.all(promises).then(() => { - console.log("%c[RE621.ModTools]%c loaded.", "color: maroon", "color: unset"); + Debug.logPrefix("Loaded."); this.buildSettingsButton(true); - console.log("%c[RE621.ModTools]%c fully loaded.", "color: maroon", "color: unset"); + Debug.logPrefix("Fully loaded."); }); } @@ -126,7 +126,7 @@ export default class REMT { */ private buildSettingsButton(addListeners = false) { this.settingsButton?.remove(); - console.log("%c[RE621.ModTools]%c checking for settings...", "color: maroon", "color: unset"); + Debug.logPrefix("Checking for settings..."); const configs = Object.values(REMT._Registry).reduce((p, e) => { if (!e) return p; if (addListeners) e.on("settingsConfigured", () => this.buildSettingsButton()); @@ -135,17 +135,17 @@ export default class REMT { return p; }, [] as SettingsDialogConfig[]); if (configs.length <= 0) { - console.log("%c[RE621.ModTools]%c no settings registered", "color: maroon", "color: unset"); + Debug.logPrefix("No settings registered"); return; } - console.log("%c[RE621.ModTools]%c loading %s settings...", "color: maroon", "color: unset", configs.length); + Debug.logPrefix("Loading %s settings...", configs.length); this.settingsButton = Util.DOM.addSettingsButton({ - id: "remt-component-settings", + id: `${Script.htmlPrefix}-component-settings`, icon: '', name: "REMT", onClick: () => MultiDialogForm.getRequestedInput(configs), }); - console.log("%c[RE621.ModTools]%c Settings button built.", "color: maroon", "color: unset"); + Debug.logPrefix("Settings button built."); } } diff --git a/src/js/components/AutoClickPosts.ts b/src/js/components/AutoClickPosts.ts index ab2f584..8615d18 100644 --- a/src/js/components/AutoClickPosts.ts +++ b/src/js/components/AutoClickPosts.ts @@ -4,7 +4,10 @@ import { html } from "../utilities/HtmlTemplate"; import Component from "./Component"; /** - * @todo What does this do? + * Automates clicking posts on the post search page; combined with the vanilla + * `Mode` selector, allows automating batch actions. + * + * @todo Add post selection mode. */ export default class AutoClickPosts extends Component { private static readonly modeBoxSelector = "#mode-box"; diff --git a/src/js/components/Component.ts b/src/js/components/Component.ts index 9f6c696..a4b3fbb 100644 --- a/src/js/components/Component.ts +++ b/src/js/components/Component.ts @@ -2,6 +2,7 @@ import REMT from "../../REMT"; import XM from "../models/api/XM"; import KeybindManager, { Keybind, ResponseFunction } from "../models/data/Keybinds"; import Page from "../models/data/Page"; +import Script from "../models/data/Script"; import Debug from "../models/Debug"; import { JsonStrictObject } from "../models/JsonTypes"; import { DialogConfig } from "../models/structure/DialogForm"; @@ -285,7 +286,7 @@ export default class Component { /** Execute all handlers for the specified component event */ public trigger(event: string, data?: PrimitiveType | PrimitiveType[] | PrimitiveMap): void { - $(document).trigger(`remt.${this.name}.${event}`, data); + $(document).trigger(`${Script.eventPrefix}.${this.name}.${event}`, data); } /** @@ -295,18 +296,18 @@ export default class Component { public on(event: string, handler: (event: JQuery.TriggeredEvent, data?: PrimitiveType | PrimitiveType[] | PrimitiveMap) => void): number { const eventList = event.split(" "); for (const one of eventList) - $(document).on(`remt.${this.name}.${one}.${this.eventIndex}`, handler); + $(document).on(`${Script.eventPrefix}.${this.name}.${one}.${this.eventIndex}`, handler); return this.eventIndex++; } /** Executes a handler function exactly once whe encountering a specified event */ public one(event: string, handler: (event: JQuery.TriggeredEvent, data?: PrimitiveType | PrimitiveType[] | PrimitiveMap) => void): void { - $(document).one(`remt.${this.name}.${event}`, handler); + $(document).one(`${Script.eventPrefix}.${this.name}.${event}`, handler); } /** Detaches handlers from the specified component event */ public off(event: string, eventID?: number): void { - $(document).off(`remt.${this.name}.${event}` + (eventID ? `.${eventID}` : "")); + $(document).off(`${Script.eventPrefix}.${this.name}.${event}` + (eventID ? `.${eventID}` : "")); } public getName(): string { return this.name; } diff --git a/src/js/components/RecordBuilder.ts b/src/js/components/RecordBuilder.ts index 6b11a0b..4af7b50 100644 --- a/src/js/components/RecordBuilder.ts +++ b/src/js/components/RecordBuilder.ts @@ -1,5 +1,6 @@ import { PageDefinition } from "../models/data/Page"; import Records from "../models/data/Records"; +import Script from "../models/data/Script"; import Util from "../utilities/Util"; import Component from "./Component"; @@ -86,12 +87,12 @@ export default class RecordBuilder extends Component { reasonSelector.val(prebuilt.reason); reasonInput .val(reason.text) - .trigger("remt:input", true); + .trigger(`${Script.eventPrefix}:input`, true); container.find("button.rules-button").removeClass("active"); for (const rule of [prebuilt.rules]) container.find("button.rules-button[name='" + rule + "']").addClass("active"); - container.trigger("remt:buttons", true); + container.trigger(`${Script.eventPrefix}:buttons`, true); this.generateRecordText(); @@ -124,16 +125,16 @@ export default class RecordBuilder extends Component { reasonSelector .appendTo(reasonContainer) - .on("change remt:change", (event, preventChange) => { + .on(`change ${Script.eventPrefix}:change`, (event, preventChange) => { const reason = Records.Reasons[reasonSelector.val() + ""]; reasonInput .val(reason.text || "") - .trigger("remt:input", preventChange); + .trigger(`${Script.eventPrefix}:input`, preventChange); container.find("button.rules-button").removeClass("active"); for (const rule of [reason.rules]) container.find("button.rules-button[name='" + rule + "']").addClass("active"); - container.trigger("remt:buttons", true); + container.trigger(`${Script.eventPrefix}:buttons`, true); this.generateRecordText(); }); @@ -154,10 +155,10 @@ export default class RecordBuilder extends Component { .on("input", (event, preventChange) => { clearTimeout(reasonInputTimer); reasonInputTimer = window.setTimeout(() => { - reasonInput.trigger("remt:input", preventChange); + reasonInput.trigger(`${Script.eventPrefix}:input`, preventChange); }, 200); }) - .on("propertychange remt:input", (event, preventChange) => { + .on(`propertychange ${Script.eventPrefix}:input`, (event, preventChange) => { container.data("reason", reasonInput.val() + ""); if (!preventChange) this.generateRecordText(); }); @@ -222,11 +223,11 @@ export default class RecordBuilder extends Component { const button = $(event.currentTarget); button.toggleClass("active"); - container.trigger("remt:buttons"); + container.trigger(`${Script.eventPrefix}:buttons`); }); } - container.on("remt:buttons", (event, preventChange) => { + container.on(`${Script.eventPrefix}:buttons`, (event, preventChange) => { const buttons: string[] = []; for (const btn of ruleWrapper.find("button.active")) buttons.push((btn as HTMLInputElement).name); @@ -240,7 +241,7 @@ export default class RecordBuilder extends Component { } private generateRecordText() { - // console.log("%c[RE621.ModTools]%c Updating Record Text", "color: maroon", "color: unset"); + // Debug.logPrefix("Updating Record Text"); const result: string[] = []; @@ -251,6 +252,8 @@ export default class RecordBuilder extends Component { this.input.val(result.join("\n")); } + private static readonly sourceMatcher = new RegExp(`https:\\/\\/(?:${Script.domains.join("|")})\\/`, "g"); + private processForm(form: JQuery): string { // console.log(form.data()); @@ -284,7 +287,7 @@ export default class RecordBuilder extends Component { (ruleData.preface ? (ruleData.preface + "\n\n") : "") + `${ruleLines.join("\n")}\n\n` + (ruleData.postface ? (ruleData.postface + "\n\n") : "") + - `"[Code of Conduct - ${ruleData.title}]":${ruleData.link ? ruleData.link : `/wiki_pages/e621:rules#${name.toLowerCase()}`}\n` + + `"[Code of Conduct - ${ruleData.title}]":${ruleData.link ? ruleData.link : `/wiki_pages/${Script.siteNameWikiPrefix}:rules#${name.toLowerCase()}`}\n` + `[/section]` ); } @@ -301,12 +304,11 @@ export default class RecordBuilder extends Component { /** * Convert a source link into a common format * @param {string} source Source link - * @todo Change to handle e6ai. */ function processSource(source: string): string { return decodeURI(source) .trim() - .replace(/https:\/\/e(?:621|926).net\//g, "/") // Make links relative + .replace(RecordBuilder.sourceMatcher, "/") // Make links relative .replace(/\/posts\/(\d+)#comment-(\d+)/g, "/comments/$2") // Convert comment links .replace(/\/forum_topics\/(\d+)(?:\?page=\d+)?#forum_post_(\d+)/g, "/forum_posts/$2") // Convert forum post links .replace(/\?lr=\d+&/, "?") // Trim the tag history links diff --git a/src/js/components/ReportContentData.ts b/src/js/components/ReportContentData.ts index 47bb77f..ab1d7af 100644 --- a/src/js/components/ReportContentData.ts +++ b/src/js/components/ReportContentData.ts @@ -1,4 +1,3 @@ -import XM from "../models/api/XM"; import { PageDefinition } from "../models/data/Page"; import Modal from "../models/structure/Modal"; import ErrorHandler from "../utilities/ErrorHandler"; @@ -150,11 +149,7 @@ export default class ReportContentData extends Component { ` as HTMLDivElement; replacer(flexBox); - const dText = await (await fetch(`/dtext_preview.json?body=${encodeURIComponent(data.body)}&allow_color="false"`, { method: "POST", "body": JSON.stringify({ "body": data.body, "allow_color": "false" }), headers: { - "User-Agent": `${XM.Info.script.name}/${XM.Info.script.version} (by ${XM.Info.script.author} on e621)`, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - "X-CSRF-Token": document.querySelector("meta[name=csrf-token]")!.getAttribute("content") ?? "", - } })).json(); + const dText = await (await fetch(`/dtext_preview.json?body=${encodeURIComponent(data.body)}&allow_color="false"`, { method: "POST", "body": JSON.stringify({ "body": data.body, "allow_color": "false" }), headers: Util.Network.simpleAuthHeaders })).json(); flexBox.removeChild(commentBox); if (dText["html"]) flexBox.appendChild(html`
Message:
${dText["html"]}
`); } diff --git a/src/js/components/RipStaffNotes.ts b/src/js/components/RipStaffNotes.ts index 55832ae..7d34d31 100644 --- a/src/js/components/RipStaffNotes.ts +++ b/src/js/components/RipStaffNotes.ts @@ -1,5 +1,5 @@ import Danbooru from "../models/api/Danbooru"; -import { StaffNote, StaffNoteExtended } from "../models/api/e621/StaffNote"; +import { StaffNote, StaffNoteExtended } from "../models/api/site/StaffNote"; import { PageDefinition } from "../models/data/Page"; import { html } from "../utilities/HtmlTemplate"; import Util from "../utilities/Util"; diff --git a/src/js/models/Debug.ts b/src/js/models/Debug.ts index b323358..201861d 100644 --- a/src/js/models/Debug.ts +++ b/src/js/models/Debug.ts @@ -1,5 +1,7 @@ import XM from "./api/XM"; +import Script from "./data/Script"; +/** @todo Support granular log levels */ export default class Debug { public static get Enabled(): boolean { return XM.Storage.getValue("Debug.enabled", false); } @@ -28,14 +30,33 @@ export default class Debug { /** Logs the provided data into the console log if debug is enabled */ - public static log(...data: any[]): void { - if (Debug.Enabled) console.log(...data); + public static log(message?: any, ...optionalParams: any[]): void { + if (Debug.Enabled) console.log(message, ...optionalParams); + } + + public static prefixAlwaysPrints = true; + /** + * Some consoles add a delimiting space between trailing concatenated + * parameters to `console.log` et al; this accounts for that. + */ + public static omitSpaceWhenNonFormatted = true; + + /** + * Logs the provided data into the console log with the colored project name + * prefix if {@link Enabled} is enabled or {@link prefixAlwaysPrints} is true. + */ + public static logPrefix(message?: any, ...optionalParams: any[]): void { + if (!Debug.prefixAlwaysPrints && !Debug.Enabled) return; + if (typeof message === "string" && /%(?:(?:\.[0-9]+)?[dif]|[oOsc])/.test(message)) + console.log(`%c[${Script.projectNameFormatted}]%c ` + message, "color: maroon", "color: unset", ...optionalParams); + else + console.log(`%c[${Script.projectNameFormatted}]%c${this.omitSpaceWhenNonFormatted ? "" : " "}`, "color: maroon", "color: unset", message, ...optionalParams); } /** Logs the provided data as a table */ - public static table(obj: any): void { + public static table(obj: any, properties?: readonly string[]): void { if (!Debug.Enabled) return; - console.table(obj); + console.table(obj, properties); } /** Logs the provided data into the console log if connections logging is enabled */ diff --git a/src/js/models/api/XMConnect.ts b/src/js/models/api/XMConnect.ts index b282053..5711722 100644 --- a/src/js/models/api/XMConnect.ts +++ b/src/js/models/api/XMConnect.ts @@ -1,4 +1,4 @@ -import Script from "../data/Script"; +import Util from "../../utilities/Util"; import Debug from "../Debug"; declare const GM_xmlhttpRequest: any; @@ -55,8 +55,8 @@ export default class XMConnect { private static validateXHRDetails(details: XMConnectRequest): XMConnectRequest { if (details.headers === undefined) details.headers = {}; if (details.headers["User-Agent"] === undefined) { - details.headers["User-Agent"] = Script.userAgent; - details.headers["X-User-Agent"] = Script.userAgent; + details.headers["User-Agent"] = Util.Network.userAgent; + details.headers["X-User-Agent"] = Util.Network.userAgent; } if (details.onabort === undefined) details.onabort = (): void => { return; }; @@ -82,8 +82,8 @@ export default class XMConnect { } if (a.headers === undefined) a.headers = { - "User-Agent": Script.userAgent, - "X-User-Agent": Script.userAgent, + "User-Agent": Util.Network.userAgent, + "X-User-Agent": Util.Network.userAgent, }; if (a.onerror === undefined) a.onerror = (): void => { return; } diff --git a/src/js/models/api/e621/StaffNote.ts b/src/js/models/api/site/StaffNote.ts similarity index 100% rename from src/js/models/api/e621/StaffNote.ts rename to src/js/models/api/site/StaffNote.ts diff --git a/src/js/models/data/Keybinds.ts b/src/js/models/data/Keybinds.ts index 0542b2a..69a496f 100644 --- a/src/js/models/data/Keybinds.ts +++ b/src/js/models/data/Keybinds.ts @@ -1,5 +1,6 @@ import Util from "../../utilities/Util"; import Debug from "../Debug"; +import Script from "./Script"; const validKeys = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", @@ -74,7 +75,7 @@ export default class KeybindManager { KeybindManager.listening = true; let keys: string[] = []; - $(document).on("keydown.remt.record", (event) => { + $(document).on(`keydown.${Script.eventPrefix}.record`, (event) => { const key = event.key .toLowerCase() .replace(replacedRegExp, (matched) => { @@ -84,9 +85,9 @@ export default class KeybindManager { keys.push(key); }); - $(document).on("keyup.remt.record", () => { + $(document).on(`keyup.${Script.eventPrefix}.record`, () => { if (keys.length !== 0) { - $(document).off(".remt.record"); + $(document).off(`.${Script.eventPrefix}.record`); callback(keys); KeybindManager.listening = false; return; @@ -132,7 +133,7 @@ export default class KeybindManager { if (!selector) selector = undefined; let cooldown: number | undefined; - $element.on("keydown.remt.hotkey-" + key, selector, key, (event: Event) => { + $element.on(`keydown.${Script.eventPrefix}.hotkey-` + key, selector, key, (event: Event) => { if (keydown) return; if (cooldown) return; @@ -150,7 +151,7 @@ export default class KeybindManager { }, 50); }); - $element.on("keyup.remt.hotkey-" + key, selector, key, () => { + $element.on(`keyup.${Script.eventPrefix}.hotkey-` + key, selector, key, () => { keydown = false; }); diff --git a/src/js/models/data/Page.ts b/src/js/models/data/Page.ts index 6e56193..14f37b5 100644 --- a/src/js/models/data/Page.ts +++ b/src/js/models/data/Page.ts @@ -1,3 +1,8 @@ +/** + * Utility for parsing the current page's URL. + * + * @todo Refactor to kill singleton; just use `window.location` directly. + */ export default class Page { private static instance: Page; @@ -9,12 +14,12 @@ export default class Page { } /** - * Checks if the url the user is currently on satisfies the filter - * @param filter Pipe separated list of filters the current location has to satisfy - * Matches are by default on a startsWith basis, but if the url must match - * you can prepend = - * @returns true if at least on filter is fulfilled - */ + * Checks if the url the user is currently on satisfies the filter + * @param filter Pipe separated list of filters the current location has to satisfy + * Matches are by default on a startsWith basis, but if the url must match + * you can prepend = + * @returns true if at least on filter is fulfilled + */ public static matches(filter: RegExp | RegExp[]): boolean { if (filter instanceof RegExp) filter = [filter]; const pathname = this.getInstance().url.pathname.replace(/[/?]$/g, ""); @@ -31,35 +36,35 @@ export default class Page { } /** - * Returns the query parameter, or null if the key does not exist - * @return string Query parameter - */ + * Returns the query parameter, or null if the key does not exist + * @return string Query parameter + */ public static getQueryParameter(key: string): string | null { return this.getInstance().url.searchParams.get(key); } /** - * Returns true if the search parameters has the provided key, false otherwise - * @param key - */ + * Returns true if the search parameters has the provided key, false otherwise + * @param key + */ public static hasQueryParameter(key: string): boolean { return this.getInstance().url.searchParams.has(key); } /** - * Sets a query parameter in the current url - * If there is already one with the same key, it will get overridden - * @param key - * @param value - */ + * Sets a query parameter in the current url + * If there is already one with the same key, it will get overridden + * @param key + * @param value + */ public static setQueryParameter(key: string, value: string): void { this.getInstance().url.searchParams.set(key, value); this.refreshCurrentUrl(); } /** - * Removes a querystring from the url - */ + * Removes a querystring from the url + */ public static removeQueryParameter(keys: string | string[]): void { if (!Array.isArray(keys)) keys = [keys]; for (const key of keys) @@ -68,8 +73,8 @@ export default class Page { } /** - * Replaces the current url without reloading or pushing the old one to the history - */ + * Replaces the current url without reloading or pushing the old one to the history + */ private static refreshCurrentUrl(): void { const url = this.getInstance().url; const searchPrefix = url.searchParams.toString().length === 0 ? "" : "?"; @@ -77,33 +82,34 @@ export default class Page { } /** - * Returns the name of the current site - * @returns e621 or e926 - */ + * Returns the name of the current site + * @returns The current site's second level (& lower, if present) domain(s). + * E.g. `e621` or `e926`. + */ public static getSiteName(): string { return this.getInstance().url.hostname.replace(/\.net/g, ""); } /** - * Returns the ID from the second part of the pathname - */ + * Returns the ID from the second part of the pathname + */ public static getPageID(): string { return this.getInstance().url.pathname.split("/")[2]; } /** - * Returns a singleton instance of the class - * @returns Url instance - */ + * Returns a singleton instance of the class + * @returns Url instance + */ public static getInstance(): Page { if (this.instance === undefined) this.instance = new Page(); return this.instance; } /** - * Returns the type of the page, according to the definitions below - * @returns Page type, as a string - */ + * Returns the type of the page, according to the definitions below + * @returns Page type, as a string + */ public static getPageName(): string | null { for (const [name, value] of Object.entries(PageDefinition as RegExpList)) { if (typeof value.test == "function") @@ -237,11 +243,6 @@ export const PageDefinition = { post: /^\/post_votes\/?/, }, - // staff_notes: { - // comment: /^\/comment_votes\/?/, - // post: /^\/post_votes\/?/, - // }, - // Custom pluginSettings: /^\/plugins\/remt.*/, }; diff --git a/src/js/models/data/Post.ts b/src/js/models/data/Post.ts index 6d13855..150af97 100644 --- a/src/js/models/data/Post.ts +++ b/src/js/models/data/Post.ts @@ -1,6 +1,7 @@ import APIPost from "@re621/zestyapi/dist/responses/APIPost"; import Util from "../../utilities/Util"; import { Tag } from "./Tag"; +import Script from "./Script"; export default class Post { @@ -293,7 +294,7 @@ export default class Post { md5: data.post.file.md5, original: data.post.file.url, sample: data.post.sample.url, - preview: `https://static1.e621.net/data/preview/${data.post.file.md5.substring(0, 2)}/${data.post.file.md5.substring(2, 4)}/${data.post.file.md5}.jpg`, + preview: `https://${Script.primaryStaticDomain}/data/preview/${data.post.file.md5.substring(0, 2)}/${data.post.file.md5.substring(2, 4)}/${data.post.file.md5}.jpg`, size: data.post.file.size, }, @@ -561,14 +562,14 @@ export default class Post { switch (type) { case "original": - return `https://static1.e621.net/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` + return `https://${Script.primaryStaticDomain}/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` case "sample": return ((options.width < 850 || options.height < 850 || options.extension == "gif") - ? `https://static1.e621.net/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` - : `https://static1.e621.net/data/sample/${parts[0]}/${parts[1]}/${parts[2]}.jpg`); + ? `https://${Script.primaryStaticDomain}/data/${parts[0]}/${parts[1]}/${parts[2]}.${parts[3]}` + : `https://${Script.primaryStaticDomain}/data/sample/${parts[0]}/${parts[1]}/${parts[2]}.jpg`); case "preview": default: { - return `https://static1.e621.net/data/preview/${parts[0]}/${parts[1]}/${parts[2]}.jpg`; + return `https://${Script.primaryStaticDomain}/data/preview/${parts[0]}/${parts[1]}/${parts[2]}.jpg`; } } } @@ -770,10 +771,10 @@ interface PostDataTypeC { url: string, }, sample: { - has: true, - height: 637, - width: 850, - url: "https://static1.e621.net/data/sample/33/6e/336ec494422113f0aaf5e879470603ac.jpg", + has: boolean, + height: number, + width: number, + url: string, alternates?: { "720p"?: AlternateType, "480p"?: AlternateType, diff --git a/src/js/models/data/Records.ts b/src/js/models/data/Records.ts index 754a202..ebdc331 100644 --- a/src/js/models/data/Records.ts +++ b/src/js/models/data/Records.ts @@ -1,3 +1,5 @@ +import Script from "./Script"; + export default class Records { @@ -31,11 +33,11 @@ export default class Records { rules: "disruptive", }, blacklist: { - text: "Please use your [[e621:blacklist|blacklist]].", + text: `Please use your [[${Script.siteNameWikiPrefix}:blacklist|blacklist]].`, rules: "blacklist", }, blacklist2: { - text: "[[e621:blacklist|Blacklist]] quietly, please.", + text: `[[${Script.siteNameWikiPrefix}:blacklist|Blacklist]] quietly, please.`, rules: "blacklist", }, roleplay: { @@ -79,7 +81,7 @@ export default class Records { rules: "sitetools", }, underage: { - text: "You need to be 18 to access this page.\nThis ban won't expire, but if you have reached the sufficient age you can request your ban to be lifted by writing an email to management@e621.net.", + text: `You need to be 18 to access this page.\nThis ban won't expire, but if you have reached the sufficient age you can request your ban to be lifted by writing an email to management@${Script.primaryDomain}.`, rules: "underage", }, }; @@ -99,7 +101,7 @@ export default class Records { disruptive: { title: "Disruptive Behavior", group: this.RuleGroups.messages, - preface: `e621 is an open and friendly community that people visit to share and enjoy furry artwork.\nPeople can have different opinions, but that does not give them permission to make someone else feel uncomfortable or unwanted.`, + preface: `${Script.primarySecondLevelDomain} is an open and friendly community that people visit to share and enjoy furry artwork.\nPeople can have different opinions, but that does not give them permission to make someone else feel uncomfortable or unwanted.`, rules: [ "Do not make messages with the apparent purpose of upsetting someone. That includes picking fights, baiting arguments, calling names, or making remarks regarding personal grievances, quarrels, or malicious rumors.", "Do not purposefully or repeatedly spread false or defamatory information.", @@ -310,9 +312,9 @@ export default class Records { underage: { title: "Age Restrictions", group: this.RuleGroups.illegal, - preface: "By using e621 and/or registering an account, you agree that you are at least eighteen years of age. No individual under the age of eighteen may register an account, use our services, provide any personal information to e621, or otherwise upload or transmit personal information through our services.", + preface: `By using ${Script.primarySecondLevelDomain} and/or registering an account, you agree that you are at least eighteen years of age. No individual under the age of eighteen may register an account, use our services, provide any personal information to ${Script.primarySecondLevelDomain}, or otherwise upload or transmit personal information through our services.`, rules: [], - postface: "Even though laws may vary depending on your location, e621.net's management and service office is located in Arizona, USA, and it is those laws that we are required to abide by. We cannot knowingly allow any minors to view pornographic content, so we must be proactive in preventing underage access. This includes accounts that are either legally or illegally accessed by minors. Furthermore, we require official, government-issued photo identification as proof of you turning eighteen in order to release access to your account. Please email us at `management@e621.net` for further details.", + postface: `Even though laws may vary depending on your location, ${Script.primaryDomain}'s management and service office is located in Arizona, USA, and it is those laws that we are required to abide by. We cannot knowingly allow any minors to view pornographic content, so we must be proactive in preventing underage access. This includes accounts that are either legally or illegally accessed by minors. Furthermore, we require official, government-issued photo identification as proof of you turning eighteen in order to release access to your account. Please email us at \`management@${Script.primaryDomain}\` for further details.`, }, }; diff --git a/src/js/models/data/Script.ts b/src/js/models/data/Script.ts index 66496ea..a5e4cc5 100644 --- a/src/js/models/data/Script.ts +++ b/src/js/models/data/Script.ts @@ -1,26 +1,147 @@ +import Util from '../../utilities/Util'; + /** - * Basic script parameters, both hard-coded and derived from the userscript header. + * Basic script parameters (both hard-coded and derived from the userscript + * header), & miscellaneous site-specific configuration. + * * GM_info is used here because of XM.Connect initialization issues. + * + * If not directly modified by downstream maintainers, expects the following from the script metadata: + * * `GM_info.script.name`: At least 2 space-separated terms. + * * `GM_info.script.version`: A proper SemVer version; can have any prefix. + * * `GM_info.script.homepage`: A proper GitHub repository URL. + * + * # IMPORTANT + * If this is modified to utilize external resources from this library (e.g. the use of {@link Util.Network.normalizeUrl}), make certain to account for circular dependencies (e.g. {@link Util.Network.userAgent} is lazily initialized). */ export default class Script { - public static version = GM_info.script.version; - public static handler = { + public static readonly displayName = GM_info.script.name; + /** + * The raw version; expected to contain a + * [SemVer](https://semver.org/)-formatted string. + * + * @deprecated Will be replaced with {@link versionObj} next major release; use {@link versionObj.raw} instead. + * @todo Replace w/ copy of `versionObj`; deprecate current `versionObj`. + */ + public static readonly version = GM_info.script.version; + /** + * An object containing the parsed version info. + */ + public static readonly versionObj = Object.freeze(Util.parseVersion(this.version)); + public static readonly handler = Object.freeze({ name: GM_info.scriptHandler, version: GM_info.version, - } - public static url = { + }); + public static readonly url = { website: "https://re621.app", - repo: "https://github.com/re621/re621.ModTools", - issues: "https://github.com/re621/re621.ModTools/issues", + repo: GM_info.script.homepage ? Util.Network.normalizeUrl(GM_info.script.homepage) : "~~HOMEPAGE_MISSING~~", + issues: (GM_info.script.homepage ? Util.Network.normalizeUrl(GM_info.script.homepage) : "~~HOMEPAGE_MISSING~~") + "/issues", thread: "https://e621.net/forum_topics/25872", latest: "https://api.github.com/repos/re621/re621.ModTools/releases/latest", kofi: "https://ko-fi.com/bitWolfy", } - public static userAgent = "re621.ModTools/" + this.trimVersion(Script.version); + // #region Internal Project Identifiers + /** + * Changes the script's name to match the pre-established console logging format. + * + * E.g. If the package's name (& consequently script's name) is `RE621 Mod + * Tools`, that becomes `RE621.ModTools`. + */ + public static readonly projectNameFormatted = Script.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, e => e.toUpperCase()) : e + "."), ""); + + /** + * Changes the script's name to match the pre-established User Agent format. + * + * E.g. If the package's name (& consequently script's name) is `RE621 Mod + * Tools`, that becomes `re621.ModTools`. + */ + public static readonly projectNameCamelFormatted = Script.displayName.split(" ").reduce((p, e) => p + (p ? e.toLowerCase().replace(/^./, e => e.toUpperCase()) : e.toLowerCase() + "."), ""); + + /** + * Changes the script's name to match an old pre-established event prefix format. + * + * E.g. If the package's name (& consequently script's name) is `RE621 Mod + * Tools`, that becomes `re621-mt`. + */ + public static readonly eventPrefixLegacy = Script.displayName.split(" ").reduce((p, e) => p + (p ? e[0].toLowerCase() : e.toLowerCase() + "-"), ""); + + /** + * A field storing the shorthand name generally used to refer to this + * userscript. Originally `REMT`. + */ + public static readonly shortName = "REMT"; + /** + * Changes the script's {@link shortName} to match the pre-established + * shorthand prefixed name format used to distinguish this userscript's events + * from others. + * + * E.g. For a {@link shortName} of `REMT`, `remt`. + */ + public static readonly eventPrefix = this.shortName.toLowerCase(); + /** + * Changes the script's {@link shortName} to match the pre-established + * shorthand name format used to distinguish this userscript's HTML elements + * from others. + * + * E.g. For a {@link shortName} of `REMT`, `remt`. + */ + public static readonly htmlPrefix = this.shortName.toLowerCase(); + // #endregion Internal Project Identifiers + /** + * + * @deprecated Use {@link Util.Network.userAgent} instead. + */ + public static readonly userAgent = this.projectNameFormatted + this.trimVersion(Script.versionObj.raw); + + /** + * @deprecated Use {@link Util.parseVersion} instead + */ private static trimVersion(value: string): string { const match = value.match(/(\d\.\d+)\.\d+/); if (!match || !match[1]) return "0.0"; return match[1]; } + + // #region Domains + /** E.g. For e621, should be `["e621.net", "e926.net"]`. */ + public static readonly domains = Object.freeze( + GM_info + .script + .matches + .map(e => e.match(/^https:\/\/(.+)\/\*/)?.[1]).reduce((p,e) => { + if (e) p.push(e); + return p; + }, []), + ); + public static readonly primaryDomain = this.domains[0] ?? "e621.net"; + + /** E.g. for e621, should be `["static1.e621.net", "static1.e926.net", "*"]` */ + public static readonly staticDomains = Object.freeze( + GM_info.script.options.override.orig_connects.filter(e => e && e !== "*"), + ); + public static readonly primaryStaticDomain = this.staticDomains[0] ?? `static1.${this.primaryDomain}`; + + /** E.g. For e621, should be `["e621", "e926"]`. */ + public static readonly secondLevelDomains = Object.freeze( + this.domains.map(e => e.split(".").at(-2) ?? e.split(".")[0] ?? e), + ); + public static readonly primarySecondLevelDomain = this.secondLevelDomains[0] ?? "e621"; + + /** + * What do important wiki pages have as their prefix by convention? + * + * E.g. For e621 & e926, `e621` (as used for `[[e621:rules]]`, et. al.). + */ + public static readonly siteNameWikiPrefix = this.primarySecondLevelDomain; + + // #region Misc + /** + * Used to reference the name of the companion userscript; should one exist. + * + * This project started as a slimmer version of re621; this is used for log + * messages related to its expected presence or lack thereof. + */ + public static readonly companionProjectName = "re621"; + // #endregion Misc } diff --git a/src/js/models/structure/Form.ts b/src/js/models/structure/Form.ts index 599617d..9007e84 100644 --- a/src/js/models/structure/Form.ts +++ b/src/js/models/structure/Form.ts @@ -2,12 +2,16 @@ import Component from "../../components/Component"; import Util from "../../utilities/Util"; import XM from "../api/XM"; import KeybindManager from "../data/Keybinds"; +import Script from "../data/Script"; import Debug from "../Debug"; import PreparedStructure from "./PreparedStructure"; /** * Form Engine v.2.0 * Simplifies the process of creating complex, visually consistent forms. + * + * @deprecated Currently unused; until it's understood, keep it that way. + * @todo Figure this thing out & use it or remove it. */ export class Form implements PreparedStructure { @@ -23,12 +27,12 @@ export class Form implements PreparedStructure { private readonly inputList: Map>; /** - * Prepares the form structure and settings. - * Use `get()` to return an actual DOM element. - * @param options Form options - * @param content Form elements - * @param onSubmit Form submission callback - */ + * Prepares the form structure and settings. + * Use `get()` to return an actual DOM element. + * @param options Form options + * @param content Form elements + * @param onSubmit Form submission callback + */ public constructor( options: SectionOptions = {}, private readonly content: FormElement[] = [], @@ -59,9 +63,9 @@ export class Form implements PreparedStructure { } /** - * Builds and returns the form DOM element - * @param force If true, ignores any cached data and rebuilds the structure from scratch - */ + * Builds and returns the form DOM element + * @param force If true, ignores any cached data and rebuilds the structure from scratch + */ public render(force = false): JQuery { if (this.created && !force) { return this.element; } @@ -91,9 +95,9 @@ export class Form implements PreparedStructure { } /** - * Resets the elements to their default values. - * Does not include buttons and submit elements - */ + * Resets the elements to their default values. + * Does not include buttons and submit elements + */ public reset(): void { this.inputList.forEach((input) => { const defval = input.attr("defval"); @@ -105,10 +109,10 @@ export class Form implements PreparedStructure { } /** - * Returns a list of elements in the form. - * If no parameter is specified, returns all inputs - * @param names Names of inputs to return - */ + * Returns a list of elements in the form. + * If no parameter is specified, returns all inputs + * @param names Names of inputs to return + */ public getInputList(...names: string[]): Map> { if (names.length == 0) { return this.inputList; } const results: Map> = new Map(); @@ -121,9 +125,9 @@ export class Form implements PreparedStructure { } /** - * An empty, placeholder form. Used to properly space out and center modals - * @param width Form width - */ + * An empty, placeholder form. Used to properly space out and center modals + * @param width Form width + */ public static placeholder(width = 1): JQuery { return new Form( { columns: width, width: width }, @@ -132,10 +136,10 @@ export class Form implements PreparedStructure { } /** - * Creates a form section FormElement based on the provided parameters - * @param options Section configuration - * @param content Form elements - */ + * Creates a form section FormElement based on the provided parameters + * @param options Section configuration + * @param content Form elements + */ public static section(options: SectionOptions = {}, content?: FormElement[]): FormElement { if (!options.name) options.name = Util.ID.make(); if (!options.columns) options.columns = 1; @@ -159,10 +163,10 @@ export class Form implements PreparedStructure { } /** - * Creates a collapsible section FormElement based on the provided parameters - * @param options Section configuration - * @param content Form elements - */ + * Creates a collapsible section FormElement based on the provided parameters + * @param options Section configuration + * @param content Form elements + */ public static accordion(options: SectionOptions & { active?: boolean | number; collapsible?: boolean } = {}, content?: FormAccordionElement[]): FormElement { if (!options.name) options.name = Util.ID.make(); if (!options.columns) options.columns = 1; @@ -228,10 +232,10 @@ export class Form implements PreparedStructure { } /** - * Creates a collapsible section FormElement based on the provided parameters - * @param options Section configuration - * @param content Form elements - */ + * Creates a collapsible section FormElement based on the provided parameters + * @param options Section configuration + * @param content Form elements + */ public static collapse(options: SectionOptions & { title?: string; badge?: JQuery; collapsed?: boolean } = {}, content?: FormElement[], onActivate?: (id: string, state: boolean) => void): FormElement { if (!options.name) options.name = Util.ID.make(); if (!options.columns) options.columns = 1; @@ -281,10 +285,10 @@ export class Form implements PreparedStructure { } /** - * Creates an input FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates an input FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static input(options: InputElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -348,10 +352,10 @@ export class Form implements PreparedStructure { } /** - * Creates an input FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates an input FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static textarea(options: InputElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -416,9 +420,9 @@ export class Form implements PreparedStructure { } /** - * Creates a copy input FormElement based on the provided parameters - * @param options Element configuration - */ + * Creates a copy input FormElement based on the provided parameters + * @param options Element configuration + */ public static copy(options: ElementOptions = {}): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -484,9 +488,9 @@ export class Form implements PreparedStructure { } /** - * Creates a key input FormElement based on the provided parameters - * @param options Element configuration - */ + * Creates a key input FormElement based on the provided parameters + * @param options Element configuration + */ public static key(options: ElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -615,11 +619,11 @@ export class Form implements PreparedStructure { } /** - * Creates a file input FormElement based on the provided parameters - * @param options Element configuration - * TODO Changed function type - * IDEA: Type to accepted MIME types? - */ + * Creates a file input FormElement based on the provided parameters + * @param options Element configuration + * TODO Changed function type + * IDEA: Type to accepted MIME types? + */ public static file(options: ElementOptions & { accept: string }, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -716,7 +720,7 @@ export class Form implements PreparedStructure { // When the field value is set externally, this event needs to be triggered on the text input field. // There is probably a better way to do this, but this should work for now. - $input.on("remt:form:update", () => { + $input.on(`${Script.eventPrefix}:form:update`, () => { console.log("updating", $input.val()); if ($input.val() == "") { $selectContainer.find("a").first().trigger("click"); } else { $selectContainer.find("a[data-value='" + $input.val() + "']").first().trigger("click"); } @@ -726,10 +730,10 @@ export class Form implements PreparedStructure { } /** - * Creates a button FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates a button FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static button(options: ElementOptions & { "type"?: "submit" | "button" } = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -779,10 +783,10 @@ export class Form implements PreparedStructure { } /** - * Creates a checkbox FormElement based on the provided parameters - * @param options Element configuration - * @param changed Input change callback - */ + * Creates a checkbox FormElement based on the provided parameters + * @param options Element configuration + * @param changed Input change callback + */ public static checkbox(options: ElementOptions = {}, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -843,11 +847,11 @@ export class Form implements PreparedStructure { } /** - * Creates a select FormElement based on the provided parameters - * @param options Element configuration - * @param content Select options data - * @param changed Input change callback - */ + * Creates a select FormElement based on the provided parameters + * @param options Element configuration + * @param content Select options data + * @param changed Input change callback + */ public static select(options: ElementOptions = {}, content?: SelectOptionSet | SelectOptionFunction, changed?: InputChangeEvent): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -904,10 +908,10 @@ export class Form implements PreparedStructure { } /** - * Creates a header FormElement based on the provided parameters - * @param text Header text - * @param width Element span - */ + * Creates a header FormElement based on the provided parameters + * @param text Header text + * @param width Element span + */ public static header(text: string, width?: number): FormElement { const $element = FormUtils.makeInputWrapper(undefined, undefined, width); @@ -921,9 +925,9 @@ export class Form implements PreparedStructure { } /** - * Creates a div FormElement based on the provided parameters - * @param options Element configuration - */ + * Creates a div FormElement based on the provided parameters + * @param options Element configuration + */ public static div(options: ElementOptions = {}): FormElement { if (!options.name) options.name = Util.ID.make(); @@ -957,10 +961,10 @@ export class Form implements PreparedStructure { } /** - * Alias for `Form.div` with streamlined options - * @param text Div contents - * @param width Wrapper width - */ + * Alias for `Form.div` with streamlined options + * @param text Div contents + * @param width Wrapper width + */ public static text(text: string, width = 1, wrapper?: string): FormElement { return Form.div({ value: text, width: width, wrapper: wrapper }); } @@ -970,19 +974,19 @@ export class Form implements PreparedStructure { } /** - * Alias for `Form.div` with some pre-built markup - * @param header First line of the header - * @param subheader Second line of the header - * @param width Wrapper width - */ + * Alias for `Form.div` with some pre-built markup + * @param header First line of the header + * @param subheader Second line of the header + * @param width Wrapper width + */ public static subheader(header: string, subheader: string, width = 1, name?: string, wrapper?: string): FormElement { return Form.div({ value: `${header}
${subheader}`, width: width, wrapper: "subheader" + (wrapper ? " " + wrapper : ""), name: name }); } /** - * Creates an hr FormElement based on the provided parameters - * @param width Element width - */ + * Creates an hr FormElement based on the provided parameters + * @param width Element width + */ public static hr(width?: number): FormElement { const $element = FormUtils.makeInputWrapper(undefined, undefined, width); @@ -995,9 +999,9 @@ export class Form implements PreparedStructure { } /** - * Creates an empty spacer element - * @param width Element width - */ + * Creates an empty spacer element + * @param width Element width + */ public static spacer(width?: number, unmargin = false): FormElement { const $element = FormUtils.makeInputWrapper(undefined, undefined, width); @@ -1035,15 +1039,15 @@ export class FormElement { private created = false; /** - * Constructs a form element based on provided data. - * Only the `wrapper` parameter is required, everything else is optional. - * @param wrapper Element that wraps the rest of the inputs - * @param input Input element, if there is one - * @param label Input label, if there is one - * @param content Contains other FormElements that need to be built and appended to this element - * @param container Container object for content. If omitted, defaults to wrapper - * @param postProcessing Function to run after the object has been built. Takes the wrapper as a parameter. - */ + * Constructs a form element based on provided data. + * Only the `wrapper` parameter is required, everything else is optional. + * @param wrapper Element that wraps the rest of the inputs + * @param input Input element, if there is one + * @param label Input label, if there is one + * @param content Contains other FormElements that need to be built and appended to this element + * @param container Container object for content. If omitted, defaults to wrapper + * @param postProcessing Function to run after the object has been built. Takes the wrapper as a parameter. + */ public constructor( private readonly wrapper: JQuery, private readonly input?: JQuery, diff --git a/src/js/models/structure/Modal.ts b/src/js/models/structure/Modal.ts index 066b8e9..eec782c 100644 --- a/src/js/models/structure/Modal.ts +++ b/src/js/models/structure/Modal.ts @@ -1,5 +1,7 @@ import Util from "../../utilities/Util"; import { makeIcon } from "../../utilities/UtilIcons"; +import Script from "../data/Script"; +import Debug from "../Debug"; import PreparedStructure from "./PreparedStructure"; export default class Modal { @@ -100,10 +102,10 @@ export default class Modal { position: { my: "center", at: "center" }, }; */ /** - * Parses the configuration and sets the default values for missing entries - * @param config Configuration to parse - * @todo Convert to using `Object.assign` with a default object instead. - */ + * Parses the configuration and sets the default values for missing entries + * @param config Configuration to parse + * @todo Convert to using `Object.assign` with a default object instead. + */ private validateConfig(config: ModalConfig): RequiredModalConfig | Required { // const result: ModalConfig = Object.assign({}, Modal.defaultConfig, config); @@ -134,26 +136,26 @@ export default class Modal { } /** - * Appends more content to the modal - * @param $content Content to add - */ + * Appends more content to the modal + * @param $content Content to add + */ public addContent($content: JQuery): void { this.$modal.append($content); } /** - * Sets the modal content - * @param $content Content to add - */ + * Sets the modal content + * @param $content Content to add + */ public setContent($content: JQuery): void { this.$modal.html(""); this.$modal.append($content); } /** - * Listens to the specified element in order to trigger the modal - * @param trigger Element-event pair to listen to - */ + * Listens to the specified element in order to trigger the modal + * @param trigger Element-event pair to listen to + */ public registerTrigger(trigger: ModalTrigger | ModalTrigger[]): void { if (!trigger) return; else if (Array.isArray(trigger)) { @@ -165,7 +167,7 @@ export default class Modal { if (this.triggers.length == 0) this.activeTrigger = trigger.element; this.triggers.push(trigger); - trigger.element.on(trigger.event + ".remt.dialog-" + this.id, (event) => { + trigger.element.on(trigger.event + `.${Script.eventPrefix}.dialog-` + this.id, (event) => { if (this.isDisabled) return; event.preventDefault(); @@ -182,7 +184,7 @@ export default class Modal { public clearTriggers(): void { for (const trigger of this.triggers) - trigger.element.off(trigger.event + ".remt.dialog-" + this.id); + trigger.element.off(trigger.event + `.${Script.eventPrefix}.dialog-` + this.id); this.triggers = []; } @@ -194,27 +196,29 @@ export default class Modal { /** - * Completely and irreversibly destroys the modal window - */ + * Completely and irreversibly destroys the modal window + */ public destroy(): void { this.$modal.dialog("destroy"); this.$modal.remove(); } /** - * Returns the element that triggered the modal - * @returns JQuery trigger - */ + * Returns the element that triggered the modal + * @returns JQuery trigger + */ public getActiveTrigger(): JQuery | undefined { return this.activeTrigger; } /** - * This class doesn't work unless re621 has added the `#modal-container` element, so this manually ensures it's added to enable it to work w/o re621. + * This class doesn't work unless the companion script has added the + * `#modal-container` element, so this manually ensures it's added to enable + * it to work w/o the companion script. */ private static ensureModalContainer() { if (!document.querySelector("#modal-container")) { - console.log("%c[RE621.ModTools]%c: no re621 detected; manually creating 'div#modal-container'...", "color: maroon", "color: unset"); + Debug.logPrefix(`No ${Script.companionProjectName} detected; manually creating 'div#modal-container'...`); $("
").attr("id", "modal-container").prependTo("div#page"); } } diff --git a/src/js/utilities/ErrorHandler.ts b/src/js/utilities/ErrorHandler.ts index 6db3809..fabc2d4 100644 --- a/src/js/utilities/ErrorHandler.ts +++ b/src/js/utilities/ErrorHandler.ts @@ -7,11 +7,11 @@ export default class ErrorHandler { public static async write(message: string, error?: Error | any): Promise { const notice = $("
").html([ - `

RE621.ModTools had encountered an error during script execution.

`, + `

${Script.projectNameFormatted} had encountered an error during script execution.

`, `

Please, report this message, including the error log below, through the issue tracker, or in the forum thread.

`, ].join("\n")); const textarea = $("