Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

MediaWiki:Gadget-CustomSearchCommands.js

MediaWiki interface page

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
//See: https://github.com/Yellow-Dog-Man/resonite-wiki/issues/33
mw.messages.set({
    'citizen-command-palette-type-component-search': 'Component Search',
    'citizen-command-palette-type-protoflux-search': 'ProtoFlux Search'
});

// https://github.com/StarCitizenTools/mediawiki-skins-Citizen/blob/main/resources/skins.citizen.commandPalette/types.js for types

function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

function createFuzzySearchCommand(config) {
    const { id, triggers, description, namespace, namespaceLabel, fuzziness } = config;

    // Cache for storing search results to avoid duplicate requests
    const searchCache = new Map();

    const checkCache = function (key) {
        if (searchCache.has(key))
            return searchCache.get(key);
        return [];
    }

    const debouncedSearch = debounce((subQuery, resolve, reject) => {
        const api = new mw.Api();

        const cacheKey = `${namespace}:${subQuery}`;
        const fuzzyQuery = `${subQuery}~${fuzziness}`;

        // Check cache first
        const cachedResults = checkCache(cacheKey);
        if (cachedResults.length > 0)
        {
            resolve(cachedResults);
            return;
        }

        api.get({
            action: 'query',
            list: 'search',
            srsearch: fuzzyQuery,
            srnamespace: namespace,
            srlimit: 10,
            srprop: 'snippet|titlesnippet',
            format: 'json'
        }).then(response => {
            if (!response.query || !response.query.search || response.query.search.length === 0) {
                const results = [{
                    id: `${id}-no-results`,
                    label: `No ${namespaceLabel} found for "${subQuery}"`,
                    description: 'Try a different search term',
                    type: id
                }];
                resolve(results);
                return;
            }

            const results = response.query.search.map(result => {

                let resultDescription = namespaceLabel;
                if (result.snippet) {
                    // Remove all HTML tags from snippet response
                    resultDescription = result.snippet.replace(/<\/?[^>]+(>|$)/g, "");
                }

                return {
                    id: `${id}-${result.pageid}`,
                    label: result.title,
                    description: resultDescription,
                    url: mw.util.getUrl(result.title),
                    highlightQuery: true,
                    type: id,
                    source: 'search'
                };
            });

            // Cache results
            searchCache.set(cacheKey, results);

            // Limit cache size to prevent memory issues
            // FIFO
            if (searchCache.size > 50) {
                const firstKey = searchCache.keys().next().value;
                searchCache.delete(firstKey);
            }

            resolve(results);
        }).catch(error => {
            console.error(`[${namespaceLabel}Search] API error:`, error);
            reject([{
                id: `${id}-error`,
                label: 'Search error',
                description: 'Could not complete search',
                type: id
            }]);
        });
    }, 300); // 300ms debounce delay

    return {
        id: id,
        triggers: triggers,
        description: description,

        getResults(subQuery) {
            if (!subQuery || subQuery.trim().length === 0) {
                return Promise.resolve([{
                    id: `${id}-empty`,
                    label: `Type to search ${namespaceLabel}...`,
                    description: `Fuzzy match search for ${namespaceLabel}`,
                    type: id
                }]);
            }

            // Return a promise that will be resolved by the debounced function
            return new Promise((resolve, reject) => {
                debouncedSearch(subQuery, resolve, reject);
            });
        },

        onResultSelect(item) {
            if (item.url) {
                return { action: 'navigate', payload: item.url };
            }
            return { action: 'none' };
        }
    };
}

const NS_COMPONENT = 3000;
const NS_PROTOFLUX = 3002;

// Create the ProtoFlux search command
const protofluxSearchCommand = createFuzzySearchCommand({
    id: 'protoflux-search',
    triggers: ['/pf:', '/protoflux:'],
    description: 'Fuzzy search ProtoFlux Nodes',
    namespace: NS_PROTOFLUX,
    namespaceLabel: 'ProtoFlux Nodes',
    fuzziness: 2
});

// Create the Component search command
const componentSearchCommand = createFuzzySearchCommand({
    id: 'component-search',
    triggers: ['/comp:', '/component:'],
    description: 'Fuzzy search Components',
    namespace: NS_COMPONENT,
    namespaceLabel: 'Components',
    fuzziness: 2
});

// Register both commands
mw.loader.using('skins.citizen.commandPalette').then(() => {
    mw.hook('skins.citizen.commandPalette.registerCommand').add((registrationData) => {
        if (!registrationData || !registrationData.registerCommand)
            return;
        registrationData.registerCommand(protofluxSearchCommand);
        registrationData.registerCommand(componentSearchCommand);
    });
});