UI Tweaks By web crawler 1 installs Rating 0.0 (0) approved

Neopets - SDB ALL Quantity Buttons

Adds an ALL button to each Safety Deposit Box quantity stepper and an ALL Quantities on Page bulk button.
bulk neopets safety deposit box sdb
https://www.scriptneo.com/script/neopets-sdb-all-quantity-buttons

Version selector


SHA256
f24e95d8f0432d6fa4e0b8fb2d5205fa125a2695111a59b73fb130b0d529a618
No scan flags on this version.

Source code

// ==UserScript==
// @name         Neopets - SDB ALL Quantity Buttons
// @namespace    https://www.scriptneo.com/
// @version      1.0.0
// @description  Adds an ALL button to each Safety Deposit Box quantity stepper and an ALL Quantities on Page bulk button.
// @author       Scriptneo
// @license      MIT
// @match        *://www.neopets.com/safetydeposit.phtml*
// @match        *://neopets.com/safetydeposit.phtml*
// @grant        none
// @run-at       document-idle
// @downloadURL  https://www.scriptneo.com/scripts/download.php?id=48
// @updateURL    https://www.scriptneo.com/scripts/download.php?id=48
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_ID = 'sdb-all-quantity-tools';
    const PAGE_TOOLBAR_ID = `${SCRIPT_ID}-toolbar`;
    const INLINE_BUTTON_CLASS = `${SCRIPT_ID}-inline`;
    const STATUS_ID = `${SCRIPT_ID}-status`;

    let scanQueued = false;

    function addStyles() {
        if (document.getElementById(`${SCRIPT_ID}-styles`)) return;

        const style = document.createElement('style');
        style.id = `${SCRIPT_ID}-styles`;
        style.textContent = `
            #${PAGE_TOOLBAR_ID} {
                display: flex;
                align-items: center;
                justify-content: center;
                flex-wrap: wrap;
                gap: 10px;
                margin: 12px 0;
                padding: 10px 12px;
                border: 1px solid rgba(72, 51, 13, 0.28);
                border-radius: 8px;
                background: rgba(255, 247, 224, 0.96);
                box-sizing: border-box;
            }

            #${PAGE_TOOLBAR_ID} .sdb-all-page-btn,
            .${INLINE_BUTTON_CLASS} {
                border: 1px solid #7b4f10;
                border-radius: 5px;
                background: linear-gradient(#ffd75a, #efa91d);
                color: #2d210d;
                font-family: Arial, sans-serif;
                font-weight: 700;
                cursor: pointer;
                box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
            }

            #${PAGE_TOOLBAR_ID} .sdb-all-page-btn {
                min-height: 36px;
                padding: 7px 15px;
                font-size: 13px;
            }

            .${INLINE_BUTTON_CLASS} {
                min-width: 42px;
                height: 30px;
                margin-left: 5px;
                padding: 0 7px;
                font-size: 11px;
                line-height: 28px;
                vertical-align: middle;
            }

            #${PAGE_TOOLBAR_ID} .sdb-all-page-btn:hover,
            .${INLINE_BUTTON_CLASS}:hover {
                filter: brightness(1.05);
            }

            #${PAGE_TOOLBAR_ID} .sdb-all-page-btn:active,
            .${INLINE_BUTTON_CLASS}:active {
                transform: translateY(1px);
            }

            #${PAGE_TOOLBAR_ID} .sdb-all-page-btn:disabled,
            .${INLINE_BUTTON_CLASS}:disabled {
                cursor: not-allowed;
                filter: grayscale(0.7);
                opacity: 0.6;
            }

            #${STATUS_ID} {
                flex-basis: 100%;
                min-height: 18px;
                color: #48330d;
                font-family: Arial, sans-serif;
                font-size: 12px;
                font-weight: 600;
                text-align: center;
            }

            .sdb-cell-stepper .${INLINE_BUTTON_CLASS} {
                display: inline-flex;
                align-items: center;
                justify-content: center;
            }

            @media (max-width: 700px) {
                #${PAGE_TOOLBAR_ID} {
                    margin-left: 6px;
                    margin-right: 6px;
                }

                #${PAGE_TOOLBAR_ID} .sdb-all-page-btn {
                    width: 100%;
                }
            }
        `;
        document.head.appendChild(style);
    }

    function parseInteger(value) {
        const match = String(value ?? '').replace(/,/g, '').match(/\d+/);
        return match ? Number(match[0]) : 0;
    }

    function findInput(stepperArea) {
        if (!stepperArea) return null;

        return stepperArea.querySelector(
            'input[type="number"], input[inputmode="numeric"], input[type="text"], input:not([type])'
        );
    }

    function findMaximum(stepperArea, input) {
        const candidates = [
            input?.max,
            input?.getAttribute('max'),
            input?.getAttribute('aria-valuemax'),
            stepperArea?.getAttribute('data-max'),
            stepperArea?.querySelector('[aria-valuemax]')?.getAttribute('aria-valuemax'),
        ];

        for (const candidate of candidates) {
            const parsed = parseInteger(candidate);
            if (parsed > 0) return parsed;
        }

        const row = stepperArea?.closest('tr');
        const desktopQty = parseInteger(row?.querySelector('.sdb-qty-cell')?.textContent);
        if (desktopQty > 0) return desktopQty;

        const mobileQty = parseInteger(row?.querySelector('.sdb-item-qty-mobile')?.textContent);
        if (mobileQty > 0) return mobileQty;

        return 0;
    }

    function setInputValue(input, value) {
        if (!input || !Number.isFinite(value) || value < 0) return false;

        const nextValue = String(Math.floor(value));
        const prototype = Object.getPrototypeOf(input);
        const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value')
            || Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');

        if (descriptor?.set) {
            descriptor.set.call(input, nextValue);
        } else {
            input.value = nextValue;
        }

        // NpNumericStepper emits Vue's update:modelValue from its native input
        // handler. Dispatch both events so the visible field and Vue state agree.
        input.dispatchEvent(new InputEvent('input', {
            bubbles: true,
            inputType: 'insertText',
            data: nextValue,
        }));
        input.dispatchEvent(new Event('change', { bubbles: true }));

        return parseInteger(input.value) === Math.floor(value);
    }

    function getStepperAreaFromPlusButton(plusButton) {
        return plusButton.closest('.sdb-cell-stepper')
            || plusButton.closest('.np-numeric-stepper')
            || plusButton.parentElement;
    }

    function setStepperToMaximum(stepperArea) {
        const input = findInput(stepperArea);
        const maximum = findMaximum(stepperArea, input);

        if (!input || maximum <= 0) return false;
        return setInputValue(input, maximum);
    }

    function findPlusButtons(root = document) {
        return [...root.querySelectorAll('button.np-stepper-btn')].filter((button) => {
            const label = `${button.textContent || ''} ${button.getAttribute('aria-label') || ''} ${button.title || ''}`
                .trim()
                .toLowerCase();
            return label === '+'
                || label.includes('increase')
                || label.includes('increment')
                || (button.textContent || '').trim() === '+';
        });
    }

    function addInlineAllButtons() {
        for (const plusButton of findPlusButtons()) {
            if (plusButton.dataset.sdbAllQuantityAttached === '1') continue;

            const existing = plusButton.parentElement?.querySelector(`.${INLINE_BUTTON_CLASS}`);
            if (existing) {
                plusButton.dataset.sdbAllQuantityAttached = '1';
                continue;
            }

            const allButton = document.createElement('button');
            allButton.type = 'button';
            allButton.className = INLINE_BUTTON_CLASS;
            allButton.textContent = 'ALL';
            allButton.title = 'Set this item to the full quantity in your Safety Deposit Box';
            allButton.setAttribute('aria-label', 'Set this item quantity to all');

            allButton.addEventListener('click', (event) => {
                event.preventDefault();
                event.stopPropagation();

                const stepperArea = getStepperAreaFromPlusButton(plusButton);
                const success = setStepperToMaximum(stepperArea);

                if (!success) {
                    setStatus('Could not read the maximum quantity for this item.', true);
                    return;
                }

                const row = plusButton.closest('tr');
                const itemName = row?.querySelector('.sdb-item-name')?.textContent?.trim();
                setStatus(itemName ? `${itemName} set to ALL.` : 'Quantity set to ALL.');
            });

            plusButton.insertAdjacentElement('afterend', allButton);
            plusButton.dataset.sdbAllQuantityAttached = '1';
        }
    }

    function findPageSteppers() {
        return [...document.querySelectorAll('.sdb-table tbody .sdb-cell-stepper')]
            .filter((cell) => findInput(cell));
    }

    function selectAllVisibleRows() {
        const masterCheckbox = document.querySelector(
            '.sdb-table thead input.sdb-item-checkbox, .sdb-list-select-all input.sdb-item-checkbox'
        );

        if (masterCheckbox && !masterCheckbox.checked) {
            masterCheckbox.click();
            return;
        }

        // Fallback for future markup changes: select one checkbox per visible row.
        for (const row of document.querySelectorAll('.sdb-table tbody tr')) {
            const checkbox = row.querySelector(
                '.sdb-col-select input.sdb-item-checkbox, input.sdb-checkbox--mobile'
            );
            if (checkbox && !checkbox.checked) checkbox.click();
        }
    }

    async function setAllQuantitiesOnPage(button) {
        button.disabled = true;
        setStatus('Setting every visible item to its full quantity...');

        try {
            selectAllVisibleRows();

            // Let Vue apply its selected-row default of 1 before replacing it
            // with the true maximum quantity.
            await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));

            let changed = 0;
            let failed = 0;

            for (const stepperArea of findPageSteppers()) {
                if (setStepperToMaximum(stepperArea)) changed += 1;
                else failed += 1;
            }

            if (changed === 0) {
                setStatus('No item quantity steppers were found. Switch to List view first.', true);
            } else if (failed > 0) {
                setStatus(`${changed} item quantities set to ALL; ${failed} could not be read.`, true);
            } else {
                setStatus(`${changed} item quantities set to ALL and selected. Choose a bulk action to continue.`);
            }
        } finally {
            button.disabled = false;
            queueScan();
        }
    }

    function setStatus(message, isError = false) {
        const status = document.getElementById(STATUS_ID);
        if (!status) return;

        status.textContent = message || '';
        status.style.color = isError ? '#a31616' : '#48330d';
    }

    function addPageToolbar() {
        if (document.getElementById(PAGE_TOOLBAR_ID)) return;

        const table = document.querySelector('.sdb-table');
        const headerBar = document.querySelector('.sdb-header-bar');
        if (!table || !headerBar) return;

        const toolbar = document.createElement('div');
        toolbar.id = PAGE_TOOLBAR_ID;

        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'sdb-all-page-btn';
        button.textContent = 'ALL Quantities on Page';
        button.title = 'Select every visible item and set each Apply Quantity field to the full owned quantity';
        button.addEventListener('click', () => setAllQuantitiesOnPage(button));

        const status = document.createElement('div');
        status.id = STATUS_ID;
        status.setAttribute('aria-live', 'polite');
        status.textContent = 'Sets all visible quantities to maximum; nothing is submitted until you confirm an action.';

        toolbar.append(button, status);
        headerBar.insertAdjacentElement('afterend', toolbar);
    }

    function scan() {
        scanQueued = false;
        if (!document.getElementById('sdb-vue-app')) return;

        addStyles();
        addPageToolbar();
        addInlineAllButtons();
    }

    function queueScan() {
        if (scanQueued) return;
        scanQueued = true;
        requestAnimationFrame(scan);
    }

    function start() {
        addStyles();
        queueScan();

        const root = document.getElementById('sdb-vue-app') || document.body;
        const observer = new MutationObserver(queueScan);
        observer.observe(root, {
            childList: true,
            subtree: true,
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', start, { once: true });
    } else {
        start();
    }
})();