diff --git a/.ispell_words b/.ispell_words index 977e6b8..b182d4a 100644 --- a/.ispell_words +++ b/.ispell_words @@ -201,6 +201,8 @@ contactTabDefinition containerItems ContainerItemsSelector ContainerItemsSelectors +containersMutationHandler +containersMutationObserver containerTimeout contentDocument contenteditable @@ -681,7 +683,6 @@ matchSelf maxCountLength maxFieldLength maxUidLength -memberof menuItem messageBox messageBoxSelector @@ -932,7 +933,6 @@ primaryContentSelector primaryNavLinksComponentRef primaryNavSelector ProfilePostConnectDrawer -proj PublicationTopLevelSection pushd px diff --git a/.jsdoc.json b/.jsdoc.json index 9e50253..62dcc3e 100644 --- a/.jsdoc.json +++ b/.jsdoc.json @@ -3,7 +3,7 @@ "private": false }, "source": { - "include": ["demos.user.js", "lib/xunit.js"] + "include": ["demos.user.js", "lib/xunit.js", "lib/base.js"] }, "plugins": ["plugins/markdown"], "markdown": { diff --git a/lib/base.js b/lib/base.js index 0fff636..c6006e8 100644 --- a/lib/base.js +++ b/lib/base.js @@ -2,7 +2,7 @@ // ==UserLibrary== // @name NH_base // @description Base library usable any time. -// @version 72 +// @version 73 // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html // @homepageURL https://github.com/nexushoratio/userscripts // @supportURL https://github.com/nexushoratio/userscripts/issues @@ -10,30 +10,50 @@ // ==/UserLibrary== // ==/UserScript== +/** + * @file Pure + * [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) + * stuff. Nothing here should be [WEB + * API](https://developer.mozilla.org/en-US/docs/Web/API) aware, except + * `Logger`'s use of `console` (and apparently `crypto`). + * @module base + * @version 73 + */ + window.NexusHoratio ??= {}; window.NexusHoratio.base = (function base() { 'use strict'; - /** @type {number} - Bumped per release. */ - const version = 72; + /** @const {number} module:base.version - Bumped per release. */ + const version = 73; - /** @type {number} - Returned by some APIs like `findIndex()`. */ + /** + * @const {number} module:base.NOT_FOUND - Returned by some APIs like + * `findIndex()`. + */ const NOT_FOUND = -1; - /** @type {number} - Useful for testing length of an array. */ + /** + * @const {number} module:base.ONE_ITEM - Useful for testing length of an + * array. + */ const ONE_ITEM = 1; /** - * @type {number} - Identify the last item in a collection like `at(-1)`. + * @const {number} module:base.LAST_ITEM - Identify the last item in a + * collection like `at(-1)`. */ const LAST_ITEM = -1; const NULL = 'null'; /** - * Mapping of Google's Canonical Error Codes. - * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto + * Mapping of Google's [Canonical Error Codes]{@link + * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto}. + * @readonly + * @enum {number} + * @alias module:base.Code */ const Code = { OK: 0, @@ -69,9 +89,10 @@ window.NexusHoratio.base = (function base() { /** * Ensures appropriate versions of NexusHoratio libraries are loaded. * - * @param {Version[]} versions - Versions required. - * @returns {object} - Namespace with only ensured libraries present. - * @throws {Error} - When requirements not met. + * @function module:base.ensure + * @param {module:base~Version[]} versions - Versions required. + * @returns {object} Namespace with only ensured libraries present. + * @throws {Error} When requirements not met. */ function ensure(versions) { let msg = 'Forgot to set a message'; @@ -168,13 +189,14 @@ window.NexusHoratio.base = (function base() { * * It takes a fixed list of event types upon construction and attempts to * use an unknown event will throw an error. + * @static */ class Dispatcher { /** - * @callback Handler + * @callback module:base.Dispatcher~Handler * @param {string} eventType - Event type. - * @param {*} data - Event data. + * @param {object} data - Event data. */ /** @@ -186,12 +208,12 @@ window.NexusHoratio.base = (function base() { } } - /** @type {boolean} - Whether dispatching is currently enabled. */ + /** @type {boolean} */ get enabled() { return this.#enabled; } - /** @param {boolean} val - Set if dispatching is currently enabled. */ + // eslint-disable-next-line require-jsdoc set enabled(val) { this.#enabled = Boolean(val); } @@ -200,8 +222,9 @@ window.NexusHoratio.base = (function base() { * Attach a function to an eventType. * * @param {string} eventType - Event type to connect with. - * @param {Handler} func - Single argument function to call. - * @returns {Dispatcher} - This instance, for chaining. + * @param {module:base.Dispatcher~Handler} func - Single argument function + * to call. + * @returns {module:base.Dispatcher} This instance, for chaining. */ on(eventType, func) { const handlers = this.#getHandlers(eventType); @@ -213,8 +236,8 @@ window.NexusHoratio.base = (function base() { * Remove all instances of a function registered to an eventType. * * @param {string} eventType - Event type to disconnect from. - * @param {Handler} func - Function to remove. - * @returns {Dispatcher} - This instance, for chaining. + * @param {module:base.Dispatcher~Handler} func - Function to remove. + * @returns {module:base.Dispatcher} This instance, for chaining. */ off(eventType, func) { const handlers = this.#getHandlers(eventType); @@ -230,7 +253,7 @@ window.NexusHoratio.base = (function base() { * * @param {string} eventType - Event type to use. * @param {object} data - Data to pass to each function. - * @returns {Dispatcher} - This instance, for chaining. + * @returns {module:base.Dispatcher} This instance, for chaining. */ fire(eventType, data) { if (this.enabled) { @@ -249,10 +272,10 @@ window.NexusHoratio.base = (function base() { * Look up array of handlers by event type. * * @param {string} eventType - Event type to look up. - * @throws {Error} - When eventType was not registered during + * @throws {Error} When eventType was not registered during * instantiation. - * @returns {Handler[]} - Handlers currently registered for this - * eventType. + * @returns {module:base.Dispatcher~Handler[]} Handlers currently + * registered for this eventType. */ #getHandlers = (eventType) => { const handlers = this.#handlers.get(eventType); @@ -491,20 +514,21 @@ window.NexusHoratio.base = (function base() { /* eslint-enable */ /** - * A simple message system that will queue messages to be delivered. + * A simple point-to-point system that will queue messages to be delivered. * * This is similar to the WEB API's `MessageChannel`. + * @static */ class MessageQueue { - /** @type {number} - Number of messages currently queued. */ + /** @type {number} */ get count() { return this.#messages.length; } /** - * @param {...*} items - Whatever to add to the queue. - * @returns {MessageQueue} - This instance, for chaining. + * @param {...object} items - Whatever to add to the queue. + * @returns {module:base.MessageQueue} This instance, for chaining. */ post(...items) { this.#messages.push(items); @@ -513,9 +537,9 @@ window.NexusHoratio.base = (function base() { } /** - * @param {?function(...*)} func - Function that receives the messages. - * If falsy, listener is removed. - * @returns {MessageQueue} - This instance, for chaining. + * @param {function(...object)} [func] - Function that receives the + * messages. If falsy, the listener is removed. + * @returns {module:base.MessageQueue} This instance, for chaining. */ listen(func) { if (func) { @@ -690,24 +714,24 @@ window.NexusHoratio.base = (function base() { this.assign(value); } - /** @returns {number} - Current value. */ + /** @returns {number} Current value. */ valueOf() { return this.#value; } - /** @returns {string} - Current value. */ + /** @returns {string} Current value. */ toString() { return `${this.valueOf()}`; } - /** @returns {number} - Current value. */ + /** @returns {number} Current value. */ toJSON() { return this.valueOf(); } /** * @param {number} value - Number to assign. - * @returns {NumberOp} - This instance. + * @returns {module:base~NumberOp} This instance. */ assign(value = 0) { this.#value = Number(value); @@ -716,7 +740,7 @@ window.NexusHoratio.base = (function base() { /** * @param {number} value - Number to add. - * @returns {NumberOp} - This instance. + * @returns {module:base~NumberOp} This instance. */ add(value) { this.#value += Number(value); @@ -822,14 +846,17 @@ window.NexusHoratio.base = (function base() { * First argument is a factory function that will create a new default value * for the key if not already present in the container. * - * The factory function may take arguments. When `.getWithArguments()` is - * used, those arguments will be passed to the factory. + * The factory function may take arguments. When {@link + * module:base.DefaultMap#getWithArguments getWithArguments} is used, those + * arguments will be passed to the factory. + * @static + * @extends Map */ class DefaultMap extends Map { /** - * @param {function(...args) : *} factory - Function that creates a new - * default value if a requested key is not present. + * @param {function(...args) : object} factory - Function that creates a + * new default value if a requested key is not present. * @param {Iterable} [iterable] - Passed to {Map} super(). */ constructor(factory, iterable) { @@ -843,8 +870,9 @@ window.NexusHoratio.base = (function base() { } /** - * @param {*} key - The key of the element to return from this instance. - * @returns {*} - The value associated with the key, perhaps newly + * @param {object} key - The key of the element to return from this + * instance. + * @returns {object} The value associated with the key, perhaps newly * created. */ get(key) { @@ -858,10 +886,11 @@ window.NexusHoratio.base = (function base() { /** * Variant of `Map.prototype.get()` that passes args to the factory. * - * @param {*} key - The key of the element to return from this instance. - * @param {...*} args - Extra arguments passed to the factory function if - * it is called. - * @returns {*} - The value associated with the key, perhaps newly + * @param {object} key - The key of the element to return from this + * instance. + * @param {...object} args - Extra arguments passed to the factory + * function if it is called. + * @returns {object} The value associated with the key, perhaps newly * created. */ getWithArguments(key, ...args) { @@ -970,52 +999,57 @@ window.NexusHoratio.base = (function base() { * Fancy-ish log messages (likely over engineered). * * Console nested message groups can be started and ended using the special - * method pairs, {@link Logger#entered}/{@link Logger#leaving} and {@link - * Logger#starting}/{@link Logger#finished}. By default, the former are - * opened and the latter collapsed (documented here as closed). + * intro/outro method pairs, {@link module:base.Logger#entered + * entered}/{@link module:base.Logger#leaving leaving} and {@link + * module:base.Logger#starting starting}/{@link module:base.Logger#finished + * finished}. By default, the former are opened and the latter collapsed + * (documented here as `closed`). * * Individual Loggers can be enabled/disabled by setting the {@link - * Logger##Config.enabled} boolean property. + * module:base.Logger~Config#enabled enabled} boolean property. * - * Each Logger will have also have a collection of {@link Logger##Group}s - * associated with it. These groups can have one of three modes: "opened", - * "closed", "silenced". The first two correspond to the browser console - * nested message groups. The intro and outro type of methods will handle - * the nesting. If a group is set as "silenced", no messages will be sent - * to the console. + * Each Logger will have also have a collection of {@link + * module:base.Logger~Group Group}s associated with it. These groups can + * have one of three modes: `opened`, `closed`, `silenced`. The first two + * correspond to the browser console nested message groups. The intro and + * outro type of methods will handle the nesting. If a group is set as + * `silenced`, no messages will be sent to the console. * * All Logger instances register a configuration with a singleton Map keyed * by the instance name. If more than one instance is created with the same * name, they all share the same configuration. * * Configurations can be exported as a plain object and reimported using the - * {@link Logger.configs} property. The object could be saved via the - * userscript manager. Depending on which one, it may have to be processed - * with the JSON.{stringify,parse} functions. Once exported, the object may - * be modified. This could be used to provide a UI to edit the object, - * though no schema is provided. + * {@link module:base.Logger.configs configs} property. The object could be + * saved via the userscript manager. Depending on which manager, it may + * have to be processed with the `JSON.{stringify,parse}` functions. Once + * exported, the object may be modified. This could be used to provide a UI + * to edit the object, though no schema is provided. * * Some values may be of interest to users for help in debugging a script. * - * The {callCount} value is how many times a logger would have been used for - * messages, even if the logger is disabled. Similarly, each group - * associated with a logger also has a {callCount}. These values can be - * used to determine which loggers and groups generate a lot of messages and - * could be disabled or silenced. + * The {@link module:base.Logger~Config#callCount callCount} value is how + * many times a logger would have been used for messages, even if the logger + * is disabled. Similarly, each group associated with a logger also has a + * {@link module:base.Logger~Group#callCount callCount}. These values can + * be used to determine which loggers and groups generate a lot of messages + * and could be disabled or silenced. * - * The {sequence} value is a rough indicator of how recently a logger or - * group was actually used. It is purposely not a timestamp, but rather, - * more closely associated with how often configurations are restored, - * e.g. during web page reloads. A low sequence number, relative to the - * others, may indicate a logger was renamed, groups removed, or simply - * parts of an application that have not been visited recently. Depending - * on the situation, one could clean up old configs, or explore other parts - * of the script. + * The {@link module:base.Logger~Config#sequence sequence} value is a rough + * indicator of how recently a logger or group was actually used. It is + * purposely not a timestamp, but rather, more closely associated with how + * often configurations are restored, e.g. during web page reloads. A low + * sequence number, relative to the others, may indicate a logger was + * renamed, groups removed, or simply parts of an application that have not + * been visited recently. Depending on the situation, one could clean up + * old configs, or explore other parts of the script. * * Handlers may be attached to listen for changes to the configuration - * singleton using the {@link Logger.onConfig} and {@link offConfig} static - * methods. Arrival of messages may controlled using {@link - * Logger.configDispatcherEnabled} (useful for disabling during testing). + * singleton using the {@link module:base.Logger.onConfig onConfig} and + * {@link module:base.Logger.offConfig offConfig} static methods. Arrival + * of messages may controlled using {@link + * module:base.Logger.configDispatcherEnabled configDispatcherEnabled} + * (useful for disabling during testing). * * @example * const log = new Logger('Bob'); @@ -1039,19 +1073,21 @@ window.NexusHoratio.base = (function base() { * GM.setValue('Logger', Logger.configs); * ... restart browser ... * Logger.configs = GM.getValue('Logger'); + * @static */ class Logger { /** - * @typedef {object} ConfigMutationRecord + * @typedef {object} module:base.Logger~ConfigMutationRecord * @property {string} logger - Name of the affected Logger instance. * @property {string?} group - Name of a possibly affected group. */ /** - * @callback ConfigMutationHandler + * @callback module:base.Logger~ConfigMutationHandler * @param {string} evt - Type of mutation. - * @param {ConfigMutationRecord} record - Details about the mutation. + * @param {module:base.Logger~ConfigMutationRecord} record - Details about + * the mutation. */ /** @param {string} loggerName - Name for this logger. */ @@ -1065,22 +1101,22 @@ window.NexusHoratio.base = (function base() { static sequence = 1; - /** @type {boolean} - Whether dispatching is currently enabled. */ + /** @type {boolean} */ static get configDispatcherEnabled() { return this.#configDispatcher.enabled; } - /** @param {boolean} val - Set if dispatching is currently enabled. */ + // eslint-disable-next-line require-jsdoc static set configDispatcherEnabled(val) { this.#configDispatcher.enabled = Boolean(val); } - /** @type {object} - Logger configurations. */ + /** @type {object} */ static get configs() { return Logger.#toPojo(); } - /** @param {object} val - Logger configurations. */ + // eslint-disable-next-line require-jsdoc static set configs(val) { const enabled = Logger.configDispatcherEnabled; Logger.configDispatcherEnabled = false; @@ -1093,7 +1129,7 @@ window.NexusHoratio.base = (function base() { } } - /** @type {string[]} - Names of known loggers. */ + /** @type {string[]} */ static get loggers() { return Array.from(this.#loggers.keys()); } @@ -1101,8 +1137,8 @@ window.NexusHoratio.base = (function base() { /** * Attach a function for all config change events. * - * @param {ConfigMutationHandler} callback - Function that receives - * events. + * @param {module:base.Logger~ConfigMutationHandler} callback - Function + * that receives events. */ static onConfig(callback) { for (const evt of this.#configEvents) { @@ -1113,8 +1149,8 @@ window.NexusHoratio.base = (function base() { /** * Remove all instances of a function attached for config change events. * - * @param {ConfigMutationHandler} callback - Function that receives - * events. + * @param {module:base.Logger~ConfigMutationHandler} callback - Function + * that receives events. */ static offConfig(callback) { for (const evt of this.#configEvents) { @@ -1126,7 +1162,7 @@ window.NexusHoratio.base = (function base() { * Get configuration of a specific Logger. * * @param {string} loggerName - Logger configuration to get. - * @returns {Logger.Config} - Current config for that Logger. + * @returns {module:base.Logger~Config} Current config for that Logger. */ static config(loggerName) { return this.#configs.getWithArguments(loggerName, loggerName); @@ -1143,27 +1179,30 @@ window.NexusHoratio.base = (function base() { this.#clear(); } - /** @type {boolean} - Whether logging is currently enabled. */ + /** + * @type {boolean} + * @alias module:base.Logger~Config#enabled + */ get enabled() { return this.#config.enabled; } - /** @type {boolean} - Indicates whether messages include a stack trace. */ + /** @type {boolean} */ get includeStackTrace() { return this.#config.includeStackTrace; } - /** @type {MessageQueue} */ + /** @type {module:base.MessageQueue} */ get mq() { return this.#mq; } - /** @type {string} - Name for this logger. */ + /** @type {string} */ get name() { return this.#name; } - /** @type {boolean} - Indicates whether current group is silenced. */ + /** @type {boolean} */ get silenced() { let ret = false; const group = this.#groupStack.at(LAST_ITEM); @@ -1178,7 +1217,7 @@ window.NexusHoratio.base = (function base() { * Log a specific message. * * @param {string} msg - Message to send to console.debug. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ log(msg, ...rest) { this.#log(msg, ...rest); @@ -1188,7 +1227,7 @@ window.NexusHoratio.base = (function base() { * Indicate entered a specific group. * * @param {string} group - Group that was entered. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ entered(group, ...rest) { this.#intro(group, Logger.#GroupMode.Opened, ...rest); @@ -1198,7 +1237,7 @@ window.NexusHoratio.base = (function base() { * Indicate leaving a specific group. * * @param {string} group - Group leaving. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ leaving(group, ...rest) { this.#outro(group, ...rest); @@ -1208,7 +1247,7 @@ window.NexusHoratio.base = (function base() { * Indicate starting a specific collapsed group. * * @param {string} group - Group that is being started. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ starting(group, ...rest) { this.#intro(group, Logger.#GroupMode.Closed, ...rest); @@ -1218,7 +1257,7 @@ window.NexusHoratio.base = (function base() { * Indicate finished with a specific collapsed group. * * @param {string} group - Group that was entered. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ finished(group, ...rest) { this.#outro(group, ...rest); @@ -1226,41 +1265,54 @@ window.NexusHoratio.base = (function base() { static #Config = class { - /** @param {string} loggerName - Name of new logger config. */ + /** + * @alias module:base.Logger~Config + * @param {string} loggerName - Name of new logger config. + */ constructor(loggerName) { this.#loggerName = loggerName; this.#fireConfigChange('logger'); } + /** + * @type {number} + * @alias module:base.Logger~Config#sequence + */ sequence = 0; - /** @type {NumberOp} */ + /** + * @type {module:base~NumberOp} + * @alias module:base.Logger~Config#callCount + */ get callCount() { return this.#callCount; } - /** @type {boolean} - Whether logging is currently enabled. */ + /** @type {boolean} */ get enabled() { return this.#enabled; } - /** @param {boolean} val - Set whether logging is currently enabled. */ + // eslint-disable-next-line require-jsdoc set enabled(val) { this.#enabled = Boolean(val); this.#fireConfigChange('enabled'); } - /** @type {Map} - Per group settings. */ + /** + * @type {Map} + * Per group settings. + */ get groups() { return this.#groups; } - /** @type {boolean} - Whether messages include a stack trace. */ + /** @type {boolean} */ get includeStackTrace() { return this.#includeStackTrace; } - /** @param {boolean} val - Set inclusion of stack traces. */ + // eslint-disable-next-line require-jsdoc set includeStackTrace(val) { this.#includeStackTrace = Boolean(val); this.#fireConfigChange('includeStackTrace'); @@ -1268,8 +1320,10 @@ window.NexusHoratio.base = (function base() { /** * @param {string} groupName - Name of the group to get. - * @param {Logger.#GroupMode} mode - Default mode if not seen before. - * @returns {Logger.#Group} - Requested group, perhaps newly made. + * @param {module:base.Logger~GroupMode} mode - Default mode if not seen + * before. + * @returns {module:base.Logger~Group} Requested group, perhaps newly + * made. */ group(groupName, mode) { const defaultMode = mode ?? 'opened'; @@ -1295,7 +1349,7 @@ window.NexusHoratio.base = (function base() { this.#fireConfigChange('used', groupName); } - /** @returns {object} - Config as a plain object. */ + /** @returns {object} Config as a plain object. */ toPojo() { const pojo = { callCount: this.callCount.valueOf(), @@ -1347,7 +1401,7 @@ window.NexusHoratio.base = (function base() { /** * @param {string} evt - Config change event to fire. * @param {string?} group - Name of group for {@link - * ConfigMutationRecord}. + * module:base.Logger~ConfigMutationRecord ConfigMutationRecord}. */ #fireConfigChange = (evt, group) => { Logger.#configDispatcher.fire(evt, { @@ -1361,8 +1415,11 @@ window.NexusHoratio.base = (function base() { static #Group = class { /** - * @param {Logger.#GroupMode} mode - Initial mode for this group. - * @param {string} logger - Name of associated {@link Logger} instance. + * @alias module:base.Logger~Group + * @param {module:base.Logger~GroupMode} mode - Initial mode for this + * group. + * @param {string} logger - Name of associated {@link + * module:base.Logger Logger} instance. * @param {string} group - Name of this group. */ constructor(mode, logger, group) { @@ -1373,17 +1430,20 @@ window.NexusHoratio.base = (function base() { this.#fireConfigChange('group'); } - /** @type {NumberOp} */ + /** + * @type {module:base~NumberOp} + * @alias module:base.Logger~Group#callCount + */ get callCount() { return this.#callCount; } - /** @type {Logger.#GroupMode} */ + /** @type {module:base.Logger~GroupMode} */ get mode() { return this.#mode; } - /** @param {Logger.#GroupMode} val - Mode to set this group. */ + // eslint-disable-next-line require-jsdoc set mode(val) { let newVal = val; if (!(newVal instanceof Logger.#GroupMode)) { @@ -1395,7 +1455,7 @@ window.NexusHoratio.base = (function base() { this.#fireConfigChange('mode'); } - /** @returns {object} - Group as a plain object. */ + /** @returns {object} Group as a plain object. */ toPojo() { const pojo = { mode: this.mode.name, @@ -1429,10 +1489,12 @@ window.NexusHoratio.base = (function base() { } - /** Enum/helper for Logger groups. */ static #GroupMode = class { /** + * Enum/helper for Logger groups. + * + * @alias module:base.Logger~GroupMode * @param {string} modeName - Mode name. * @param {string} [greeting] - Greeting when opening group. * @param {string} [farewell] - Salutation when closing group. @@ -1453,28 +1515,31 @@ window.NexusHoratio.base = (function base() { * Find GroupMode by name. * * @param {string} modeName - Mode name. - * @returns {GroupMode} - Mode, if found. + * @returns {module:base.Logger~GroupMode} Mode, if found. */ static byName(modeName) { return this.#known.get(modeName); } - /** @type {string} - Farewell when closing group. */ + /** @type {string} */ get farewell() { return this.#farewell; } - /** @type {string} - console.func to use for opening group. */ + /** + * @type {string} + * console.func to use for opening group. + */ get func() { return this.#func; } - /** @type {string} - Greeting when opening group. */ + /** @type {string} */ get greeting() { return this.#greeting; } - /** @type {string} - Mode name. */ + /** @type {string} */ get name() { return this.#name; } @@ -1534,7 +1599,7 @@ window.NexusHoratio.base = (function base() { } } - /** @returns {object} - Logger.#configs as a plain object. */ + /** @returns {object} Logger.#configs as a plain object. */ static #toPojo = () => { const pojo = { type: 'LoggerConfigs', @@ -1585,7 +1650,7 @@ window.NexusHoratio.base = (function base() { * Log a specific message. * * @param {string} msg - Message to send to console.debug. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ #log = (msg, ...rest) => { const group = this.#groupStack.at(LAST_ITEM); @@ -1604,8 +1669,8 @@ window.NexusHoratio.base = (function base() { * Introduces a specific group. * * @param {string} group - Group being created. - * @param {Logger.#GroupMode} defaultMode - Mode to use if new. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {module:base.Logger~GroupMode} defaultMode - Mode to use if new. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ #intro = (group, defaultMode, ...rest) => { this.#groupStack.push(group); @@ -1625,7 +1690,7 @@ window.NexusHoratio.base = (function base() { * Concludes a specific group. * * @param {string} group - Group leaving. - * @param {...*} rest - Arbitrary items to pass to console.debug. + * @param {...object} rest - Arbitrary items to pass to console.debug. */ #outro = (group, ...rest) => { const mode = this.#config.group(group).mode; @@ -1653,7 +1718,6 @@ window.NexusHoratio.base = (function base() { /* eslint-disable require-jsdoc */ /* eslint-disable no-undefined */ - /** This must be nested due to accessing #private fields. */ static GroupModeTestCase = class extends NH.xunit.TestCase { testClassIsFrozen() { @@ -1775,7 +1839,7 @@ window.NexusHoratio.base = (function base() { // Assert - // Some of these are {@link NumberOp} + // Some of these are {@link module:base~NumberOp} this.defaultEqual = this.equalValueOf; const config = Logger.config(this.id); @@ -1844,7 +1908,7 @@ window.NexusHoratio.base = (function base() { // This test does not strictly follow Assemble/Act/Assert as it has // extra verifications during state changes. - // Some of these are {@link NumberOp} + // Some of these are {@link module:base~NumberOp} this.defaultEqual = this.equalValueOf; // Initial @@ -1881,7 +1945,7 @@ window.NexusHoratio.base = (function base() { // This test does not strictly follow Assemble/Act/Assert as it has // extra verifications during state changes. - // Some of these are {@link NumberOp} + // Some of these are {@link module:base~NumberOp} this.defaultEqual = this.equalValueOf; const grp = 'a-loop'; @@ -1952,8 +2016,8 @@ window.NexusHoratio.base = (function base() { /** * Execute TestCase tests. * - * @param {Logger} logger - Logger to use. - * @returns {boolean} - Success status. + * @param {module:base.Logger} logger - Logger to use. + * @returns {boolean} Success status. */ function doTestCases(logger) { const me = 'Running TestCases'; @@ -1993,7 +2057,9 @@ window.NexusHoratio.base = (function base() { /** * Basic test runner. * - * This depends on {Logger}, hence the location in this file. + * This depends on {@link module:base.Logger Logger}, hence the location in + * this file instead of {@link module:xunit}. + * @function runTests */ function runTests() { if (NH.xunit.testing.enabled) { @@ -2017,8 +2083,9 @@ window.NexusHoratio.base = (function base() { /** * Create a UUID-like string with a base. * + * @function module:base.uuId * @param {string} strBase - Base value for the string. - * @returns {string} - A unique string. + * @returns {string} A unique string. */ function uuId(strBase) { return `${strBase}-${crypto.randomUUID()}`; @@ -2027,8 +2094,9 @@ window.NexusHoratio.base = (function base() { /** * Normalize a string to be safe to use as an HTML element id. * + * @function module:base.safeId * @param {string} input - The string to normalize. - * @returns {string} - Normalized string. + * @returns {string} Normalized string. */ function safeId(input) { let result = input @@ -2093,8 +2161,9 @@ window.NexusHoratio.base = (function base() { * * https://en.wikipedia.org/wiki/Jenkins_hash_function#one_at_a_time * + * @function module:base.strHash * @param {string} s - String to hash. - * @returns {string} - Hash value. + * @returns {string} Hash value. */ function strHash(s) { const max = 4294967296n; @@ -2143,8 +2212,9 @@ window.NexusHoratio.base = (function base() { /** * @deprecated Compute the SHA-256 digest of a string. * + * @function module:base.sha256 * @param {string} string - String to hash. - * @returns {string} - Hex digest of the string. + * @returns {string} Hex digest of the string. */ async function sha256(string) { const radix = 16; @@ -2206,8 +2276,9 @@ window.NexusHoratio.base = (function base() { * Empty strings return an empty array. * Extra separators are consolidated. * + * @function module:base.simpleParseWords * @param {string} text - Text to parse. - * @returns {string[]} - Parsed text. + * @returns {string[]} Parsed text. */ function simpleParseWords(text) { const results = []; @@ -2388,8 +2459,9 @@ window.NexusHoratio.base = (function base() { /** * Base class for building services that can be turned on and off. * - * Subclasses should NOT override methods here, except for constructor(). - * Instead they should register listeners for appropriate events. + * Subclasses should NOT override methods here, except for the + * `constructor`. Instead they should register listeners for appropriate + * events. * * Generally, methods will fire two event verbs. The first, in present * tense, will instruct what should happen (activate, deactivate). The @@ -2427,7 +2499,7 @@ window.NexusHoratio.base = (function base() { * .on('deactivated', dummyEventCallback); * service.activate(); * service.deactivate(); - * + * @static */ class Service { @@ -2439,17 +2511,17 @@ window.NexusHoratio.base = (function base() { this.setShortName(shortName); } - /** @type {Logger} - Logger instance. */ + /** @type {module:base.Logger} */ get logger() { return this.#logger; } - /** @type {string} - Instance name. */ + /** @type {string} */ get name() { return this.#name; } - /** @type {string} - Shorter instance name. */ + /** @type {string} */ get shortName() { return this.#shortName; } @@ -2482,9 +2554,9 @@ window.NexusHoratio.base = (function base() { * Attach a function to an eventType. * * @param {string} eventType - Event type to connect with. - * @param {Dispatcher~Handler} func - Single argument function to - * call. - * @returns {Service} - This instance, for chaining. + * @param {module:base.Dispatcher~Handler} func - Single argument function + * to call. + * @returns {module:base.Service} This instance, for chaining. */ on(eventType, func) { this.#dispatcher.on(eventType, func); @@ -2495,8 +2567,8 @@ window.NexusHoratio.base = (function base() { * Remove all instances of a function registered to an eventType. * * @param {string} eventType - Event type to disconnect from. - * @param {Dispatcher~Handler} func - Function to remove. - * @returns {Service} - This instance, for chaining. + * @param {module:base.Dispatcher~Handler} func - Function to remove. + * @returns {module:base.Service} This instance, for chaining. */ off(eventType, func) { this.#dispatcher.off(eventType, func); @@ -2506,7 +2578,7 @@ window.NexusHoratio.base = (function base() { /** * @param {boolean} allow - Whether to allow this service to be activated * when already active. - * @returns {Service} - This instance, for chaining. + * @returns {module:base.Service} This instance, for chaining. */ allowReactivation(allow) { this.#allowReactivation = allow; @@ -2515,7 +2587,7 @@ window.NexusHoratio.base = (function base() { /** * @param {string} shortName - Custom name portion of this instance. - * @returns {Service} - This instance, for chaining. + * @returns {module:base.Service} This instance, for chaining. */ setShortName(shortName) { this.#name = `${this.constructor.name}: ${shortName}`; @@ -2548,6 +2620,11 @@ window.NexusHoratio.base = (function base() { static Test = class extends Service { + /** + * @alias Test + * @param {string} myName - Testing. + * @ignore + */ constructor(myName) { super(`The ${myName}`); this.on('activate', this.#onEvent) diff --git a/lib/base.md b/lib/base.md index 5f7c336..374f97c 100644 --- a/lib/base.md +++ b/lib/base.md @@ -2,7 +2,7 @@ Pure [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript) stuff. Nothing here should be [WEB API](https://developer.mozilla.org/en-US/docs/Web/API) aware, except `Logger`'s use of `console` (and apparently `crypto`). -## Exported properties (as of version 71) +## Exported properties (as of version 73) * version - Bumped per release. * NOT_FOUND - Returned by some APIs like `findIndex()`. * ONE_ITEM - Useful for testing length of an array. diff --git a/lib/xunit.js b/lib/xunit.js index 9c33fb2..13205eb 100644 --- a/lib/xunit.js +++ b/lib/xunit.js @@ -23,10 +23,7 @@ window.NexusHoratio ??= {}; window.NexusHoratio.xunit = (function xunit() { 'use strict'; - /** - * @const {number} version - Bumped per release. - * @static - */ + /** @const {number} module:xunit.version - Bumped per release. */ const version = 63; /** @@ -108,8 +105,7 @@ window.NexusHoratio.xunit = (function xunit() { } /** - * @callback ReprFunc - * @memberof module:xunit.TypeTool~ + * @callback module:xunit.TypeTool~ReprFunc * @param {object} item - Anything. * @returns {string} String version of item. */ @@ -215,7 +211,8 @@ window.NexusHoratio.xunit = (function xunit() { } /** - * See {@link base.runTests} for an example of how this might be used. + * See {@link module:base~runTests} for an example of how this might be + * used. * * @const {object} testing - Default testing support. * @property {boolean} enabled - Whether tests should be executed. @@ -229,10 +226,16 @@ window.NexusHoratio.xunit = (function xunit() { /** Data about a test execution. */ class TestExecution { - /** @type {number} - Returned from Date.now(). */ + /** + * Returned from Date.now(). + * @type {number} + */ start = 0; - /** @type {number} - Returned from Date.now(). */ + /** + * Returned from Date.now(). + * @type {number} + */ stop = 0; } @@ -241,8 +244,7 @@ window.NexusHoratio.xunit = (function xunit() { class TestResult { /** - * @typedef {object} MethodResult - * @memberof module:xunit~TestResult~ + * @typedef {object} module:xunit~TestResult~MethodResult * @property {string} methodName - Name of the associated {@link * module:xunit.TestCase} method. * @property {string?} error - Name of caught exception. @@ -523,7 +525,7 @@ window.NexusHoratio.xunit = (function xunit() { /** * @class * @extends Error - * @memberof module:xunit.TestCase~ + * @alias module:xunit.TestCase~Error */ static Error = class extends Error { @@ -537,14 +539,14 @@ window.NexusHoratio.xunit = (function xunit() { /** * @class * @extends module:xunit.TestCase~Error - * @memberof module:xunit.TestCase~ + * @alias module:xunit.TestCase~Fail */ static Fail = class extends this.Error {} /** * @class * @extends module:xunit.TestCase~Error - * @memberof module:xunit.TestCase~ + * @alias module:xunit.TestCase~Skip */ static Skip = class extends this.Error {} @@ -849,16 +851,14 @@ window.NexusHoratio.xunit = (function xunit() { } /** - * @typedef {object} EqualOutput - * @memberof module:xunit.TestCase~ + * @typedef {object} module:xunit.TestCase~EqualOutput * @property {boolean} equal - Result of equality test. * @property {string} detail - Details appropriate to the test (e.g., * where items differed). */ /** - * @callback EqualFunc - * @memberof module:xunit.TestCase~ + * @callback module:xunit.TestCase~EqualFunc * @param {object} first - First argument. * @param {object} second - Second argument. * @returns {module:xunit.TestCase~EqualOutput} Results of testing @@ -3436,8 +3436,7 @@ diff: | /** * Run registered TestCases. * - * @function runTests - * @static + * @function module:xunit.runTests * @returns {module:xunit~TestResult} Accumulated results of these tests. */ function runTests() { diff --git a/linkedin-tool.user.js b/linkedin-tool.user.js index 78abbd1..d4b397d 100644 --- a/linkedin-tool.user.js +++ b/linkedin-tool.user.js @@ -4,7 +4,7 @@ // @match https://www.linkedin.com/* // @inject-into content // @noframes -// @version 77 +// @version 78 // @author Mike Castle // @description Minor enhancements to LinkedIn. Mostly just hotkeys. // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0-standalone.html @@ -12,8 +12,8 @@ // @supportURL https://github.com/nexushoratio/userscripts/blob/main/linkedin-tool.md // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1 // @require https://cdn.jsdelivr.net/npm/commonmark@0.31.2 -// @require https://update.greasyfork.org/scripts/478188/1877646/NH_xunit.js -// @require https://update.greasyfork.org/scripts/477290/1869418/NH_base.js +// @require https://update.greasyfork.org/scripts/478188/1879877/NH_xunit.js +// @require https://update.greasyfork.org/scripts/477290/1879852/NH_base.js // @require https://update.greasyfork.org/scripts/478349/1846239/NH_userscript.js // @require https://update.greasyfork.org/scripts/478440/1862017/NH_web.js // @require https://update.greasyfork.org/scripts/478676/1848076/NH_widget.js @@ -33,7 +33,7 @@ const NH = window.NexusHoratio.base.ensure([ {name: 'xunit', minVersion: 63}, - {name: 'base', minVersion: 72}, + {name: 'base', minVersion: 73}, {name: 'userscript', minVersion: 17}, {name: 'web', minVersion: 16}, {name: 'widget', minVersion: 50}, @@ -193,11 +193,6 @@ '2026-06-30' ), ish('349', '`LinkedIn`: Remove navbar margin support', '2026-06-20'), - ish( - '350', - '`Scroller`: Add `this` as an argument to `uidCallback()`', - '2026-06-19' - ), ish('351', '**Feed**: load more feature no longer works', '2026-06-20'), ish( '352', @@ -233,9 +228,66 @@ ish( '370', 'Menu attaching on *Style-2* pages stopped working', '2026-07-15' ), + ish( + '373', + 'Remove "Select current results page" from all pages with pagination', + '2026-07-19' + ), + ish( + '372', + '`Scroller`: New item cache fails on at least one page', + '2026-07-19' + ), ]; const globalNewsContent = [ + { + date: '2026-07-19', + issues: ['373'], + subject: 'Remove the explicit shortcut for selecting the next page' + + ' of results', + }, + { + date: '2026-07-19', + issues: ['372'], + subject: 'Temporarily disable the item cache', + }, + { + date: '2026-07-19', + issues: ['372'], + subject: 'Remove an old logging statement', + }, + { + date: '2026-07-19', + issues: ['372'], + subject: 'Rename some internal symbols', + }, + { + date: '2026-07-19', + issues: ['372'], + subject: 'Remove unnecessary intermediate array', + }, + { + date: '2026-07-19', + issues: [''], + subject: 'Fix a couple of comments', + }, + { + date: '2026-07-19', + issues: ['352'], + subject: 'Additional update on how `Profile`\'s `Scroller`s are' + + ' configured', + }, + { + date: '2026-07-19', + issues: ['302'], + subject: 'Updated `UidMode`s for the *Highlights* section', + }, + { + date: '2026-07-19', + issues: ['302'], + subject: 'Another partial ordering pair', + }, { date: '2026-07-18', issues: ['352'], @@ -796,31 +848,6 @@ issues: ['351'], subject: 'Rework how **Feeds**\'s "load most posts" feature works', }, - { - date: '2026-06-19', - issues: [''], - subject: 'Add links to GitHub issues for certain debug alerts', - }, - { - date: '2026-06-19', - issues: [''], - subject: 'Linkify release notes to the associated issue', - }, - { - date: '2026-06-19', - issues: ['350'], - subject: 'Add `this` as the first argument to `uidCallback()`', - }, - { - date: '2026-06-19', - issues: ['302'], - subject: 'Support the *Education* section', - }, - { - date: '2026-06-19', - issues: ['302'], - subject: 'Rename a variable', - }, ]; /** @@ -1295,7 +1322,9 @@ this.#validateInstance(); - this.#mutationObserver = new MutationObserver(this.#mutationHandler); + this.#containersMutationObserver = new MutationObserver( + this.#containersMutationHandler + ); this.#logger = new NH.base.Logger(`{${this.#name}}`); this.logger.log('Scroller constructed', this); @@ -1540,10 +1569,7 @@ const me = this.activate.name; this.logger.entered(me); - const containers = new Set( - Array.from(await this.#waitForContainers()) - .filter(x => x) - ); + const containers = new Set(await this.#waitForContainers()); if (this.#base) { containers.add(this.#base); } @@ -1563,10 +1589,10 @@ this.#clickOptions); } this.logger.log('observing with', container, observeOptions); - this.#mutationObserver.observe(container, observeOptions); + this.#containersMutationObserver.observe(container, observeOptions); } - this.logger.log('watcher:', await watcher); + await watcher; this.#mutationDispatcher.on('attributes', this.#attributesHandler); this.#mutationDispatcher.on('childList', this.#monitorConnectedness); @@ -1582,7 +1608,7 @@ deactivate() { this.#mutationDispatcher.off('attributes', this.#attributesHandler); this.#mutationDispatcher.off('childList', this.#monitorConnectedness); - this.#mutationObserver.disconnect(); + this.#containersMutationObserver.disconnect(); for (const container of this.#onClickElements) { container.removeEventListener('click', this.#onClick, @@ -1630,6 +1656,7 @@ #clickOptions = {capture: true}; #containerItems #containerTimeout + #containersMutationObserver #currentItem = null; #currentItemId = null; #destroyed = false; @@ -1643,7 +1670,6 @@ #logger #maxUidLength #mutationDispatcher = new NH.base.Dispatcher('attributes', 'childList'); - #mutationObserver #name #observeAttributes #onClickElements = new Set(); @@ -1731,8 +1757,8 @@ * @param {MutationRecord[]} records - Standard mutation records. * @fires 'childList' */ - #mutationHandler = (records) => { - const me = this.#mutationHandler.name; + #containersMutationHandler = (records) => { + const me = this.#containersMutationHandler.name; this.logger.entered(me, `records: ${records.length}`); const types = new NH.base.DefaultMap(Array); @@ -1779,6 +1805,9 @@ const me = this.#getItems.name; this.logger.entered(me); + // TODO(#372): Reenable the cache + this.#itemCache = null; + if (!this.#itemCache) { const items = []; if (this.#base) { @@ -2130,7 +2159,7 @@ const results = []; /** - * Simply eats any exception throw by the Promise. + * Simply eats any exception thrown by the Promise. * @param {Promise} prom - Whatever Promise we are wrapping. * @param {string} note - Put into log on error. * @returns {Promise} - Resolved promise. @@ -7197,15 +7226,6 @@ } ); - selectCurrentResultsPage = new Shortcut( - 'c', - 'Select current results page', - () => { - this.paginator.dull(); - NH.web.clickElement(this.paginator.item, ['button'], true); - } - ); - tabList = new Shortcut( 'l', 'Focus on discovery tab list (has native scrolling using arrows)', @@ -8926,9 +8946,6 @@ } /* eslint-enable */ - /** @type {UidMode[]} */ - static #entriesCurrentModes - /** * @typedef {object} ScrollerConfig * @property {Scroller~uidCallback} uidCallback - Callback to generate a @@ -9360,6 +9377,7 @@ } #checkingPartialOrder = false + #entriesCurrentModes #entriesCurrentUid #entriesScrollerConfigDefault = Symbol('default'); #entriesScrollerConfigs = new Map(); @@ -9380,6 +9398,7 @@ 'CertificationTopLevel, Skills', 'CertificationTopLevel, VolunteerExperienceTopLevel', 'ConnectedAccountsTopLevel, CertificationTopLevel', + 'ConnectedAccountsTopLevel, Skills', 'CourseTopLevelSection, Causes', 'CourseTopLevelSection, HonorsTopLevel', 'CourseTopLevelSection, Interests', @@ -9552,7 +9571,8 @@ ], modes: [ this.ctor.UidMode.COMPANY, - this.ctor.UidMode.ID, + this.ctor.UidMode.ANCHOR_PROFILE, + this.ctor.UidMode.ARIA_LABEL, this.ctor.UidMode.FOOTER, ], }); @@ -9564,7 +9584,8 @@ ], modes: [ this.ctor.UidMode.COMPANY, - this.ctor.UidMode.ID, + this.ctor.UidMode.ANCHOR_PROFILE, + this.ctor.UidMode.ARIA_LABEL, this.ctor.UidMode.FOOTER, ], }); @@ -9834,7 +9855,7 @@ }; this.#entriesCurrentUid = config.uidCallback; - this.ctor.#entriesCurrentModes = config.modes; + this.#entriesCurrentModes = config.modes; this.#entryScroller = new Scroller(what, how); this.#entryScroller.dispatcher @@ -9851,10 +9872,10 @@ */ #entriesUidFromModes = (scroller, element) => { const me = this.#entriesUidFromModes.name; - scroller.logger.entered(me, element); + this.logger.entered(me, element); const results = this.ctor.#entriesModeToUid( - scroller, element, this.ctor.#entriesCurrentModes + scroller, element, this.#entriesCurrentModes ); if (results.size === 0) { @@ -9866,7 +9887,7 @@ .entries() .next().value; - scroller.logger.leaving(me, mode, uid); + this.logger.leaving(me, mode, uid); return [mode, uid]; } @@ -10415,14 +10436,6 @@ } ); - selectCurrentResultsPage = new Shortcut( - 'c', - 'Select current results page', - () => { - NH.web.clickElement(this.paginator.item, ['button']); - } - ); - firstItem = new Shortcut( '<', 'Go to the first item', @@ -10482,7 +10495,7 @@ name: `${this.name} pagination`, containerItems: [ { - // This selector is also used in #onPaginationActivate. + // This selector is also used in #onPaginationActivate. container: 'main ul[data-testid="pagination-controls-list"]', items: ':scope > li', },