Skip to content

How to add a feature

Garden Shade edited this page Nov 28, 2020 · 8 revisions

As of v4, the modular nature of the userscript's code means adding new features has been somewhat standardized. Unless the new feature runs before the page has even loaded there will only be a few key building blocks that all features require:

  • The appropriate page module (src/modules/) to organize based on the feature's scope
  • The feature class (inside the module) which contains all of the feature's logic
  • The feature initialization inside src/features.ts

Page Modules

The page modules store all the typescript files relating to specific scopes: usually, single pages (or types of pages) that the feature will run on. These are purely for organizational purposes, as the feature's scope is determined by the feature's Class, not its directory.

There are three special modules for atypical scopes (Global, Core, and Shared) plus the typical page scopes.

Page scopes

Nearly all features will use page scopes instead of the aforementioned scopes. Simply choose the page your feature will run on, and add a new Class.

If you wish to add a new page scope, additional changes are required:

  • Add the new /modules/page.ts file
  • In types.ts, add the page to ValidPage and SettingGroup types
  • In check.ts, find the page() function and add a new entry to the cases Object. The property should be the string returned by window.location.pathname and the value should be the name of the page (as added to ValidPage)
  • Add a new comment block in feature.ts
  • Add a reference path in app.ts

Global scope

This module is for features that run on every page. Special care should be taken to ensure that the element the feature requires is present on all pages; otherwise, include a race condition when starting it.

Core

This module is for special features that meet either of the following criteria:

  1. They are important components of the userscript.
  2. They are intended to be consumed by other features.

Finally, they must meet the additional requirement that they will generate user-changeable settings on the Preferences page. If these requirements aren't all met consider using /modules/shared.ts or utils.ts instead.

Shared

Shared features are, essentially, utility functions that are too specific to be in the utils.ts file. They mainly exist to promote Don't Repeat Yourself (DRY) code, and are often used when two or three features do something nearly identical.

Shared features must be public and can be accessed by creating a new Shared() in the feature that requires it.

Feature Class

The feature class, placed inside the appropriate Page Module, contains all the logic of your feature. It requires a few key components in order to properly implement the Feature interface.

If you are using VS Code, you will have access to some custom snippets to generate a new feature. Just start typing mp to trigger suggestions for mp-check, mp-text, and mp-drop. Each one will fill out the required code for a feature of its type and will guide you through editing it.

class YourFeatureName implements Feature {

  // Choose a setting type based on how the user will interact with the feature's settings
  private _settings: [CheckboxSetting | DropdownSetting | TextboxSetting] = { /* Settings */ }

  // An element that must exist in order for the feature to run
  private _tar: string = '#';

  // The code that runs when the feature is created on `features.ts`.
  // It's typically the same on all features, but can be changed as long as `Util.startFeature(...)` runs.
  // If `startFeature` returns true, init the feature
  constructor() {
    Util.startFeature(/* settings, elem, page */)
    .then( t => { if (t) { this._init() } } );
  }

  // This is where your feature starts
  private async _init() {
    /* code */
  }

  // This must match the type selected for `this._settings`
  get settings(): [CheckboxSetting | DropdownSetting | TextboxSetting] {
    return this._settings;
  }
}

The above code is a suitable template for most features. The only key addition missing is the value of this._settings, which depends on the required setting type. These are the properties and value types they require.

CheckboxSetting

scope: SettingGroup;
title: string;
type: 'checkbox';
desc: string;

DropdownSetting

scope: SettingGroup;
title: string;
type: 'dropdown';
desc: string;
tag: string; /* Short title/info appearing before the dropdown */
options: StringObject;
/* StringObject: {
    default: "Default text",
    settingID: "Setting text",
} */

TextboxSetting

scope: SettingGroup;
title: string;
type: 'textbox';
desc: string;
tag: string; /* Short title/info appearing before the dropdown */
placeholder: string; /* Example shown in empty textbox */

Util.startFeature()

Additionally, the Util.startFeature(settings,elem,page?) values are almost always identical, but the page the feature will run on needs to be specified.

  • settings:FeatureSettings - always this._settings
  • elem:string - always this._tar
  • page?:ValidPage[] - an array of valid pages, ex. ['shoutbox','home'] Features in a page module tend to always use the same single page value. Certain features (ex. shoutbox) require more than one page.

Initialization

In order for your feature to be run, it must be added to features.ts. This page also determines the order in which settings will be generated on the Settings page. Settings will be grouped by type (not controlled by this page), and Features of one type that are called before other Features of the same type will appear first.

Simply add new YourFeatureName(); in the appropriate location inside the constructor.