first of all I wanted to thank the developers of this site for this amazing project! The background graphics, hover cards, and basically the whole UI is amazing and I love it!
I've recently picked up D2R again and used this site to look at rolls for the items I have. This means look at the item in my stash -> look up the name on the site -> check rolls -> repeat.
Since I like to do as much as possible via keybinds I was missing some keybinds for the search function (or I just didn't find them, Idk).
To this end I wrote a small user script (Tampermonkey, Violentmonkey, ...) that injects a bit of CSS and an input handler for the search field to support some basic keybinds.
The script uses https://github.com/ccampbell/mousetrap to focus the search input when pressing a Key (or Key combination).
With the user script, you can do the following (here's a demo: https://streamable.com/kam8d4)
- Press
/
to focus the search input - Type in part of the item name
- Wait for the live results to load
- Press arrow down/up to select the item
- Press enter to select it and load the page
Spoiler
Code: Select all
import Mousetrap from 'mousetrap'
import { injectCss } from '../lib'
type Callback = (mutations: MutationRecord[], observer: MutationObserver) => boolean
// Focus search box when pressing
Mousetrap.bind('/', () => {
$('input.topic-live-search').get(0)?.focus()
return false
})
const getSearchResults = () => {
return $('div.acResults > ul.undefined > span.ajax_catch_two > li.ajax_link').toArray()
}
const modifySearchbox: Callback = () => {
const searchbox = $<HTMLInputElement>('input.topic-live-search')
if (!searchbox.length) return false
searchbox.on('keydown', (e) => {
const results = getSearchResults()
if (results.length === 0) return
const currentIndex = results.findIndex((e) => $(e).hasClass('custom-selected'))
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % results.length
const prevIndex = currentIndex === -1 ? 0 : (currentIndex - 1 + results.length) % results.length
if (e.key === 'ArrowDown') {
e.preventDefault()
$(results[currentIndex]).removeClass('custom-selected')
$(results[nextIndex]).addClass('custom-selected')
} else if (e.key === 'ArrowUp') {
e.preventDefault()
$(results[currentIndex]).removeClass('custom-selected')
$(results[prevIndex]).addClass('custom-selected')
} else if (e.key === 'Enter' && currentIndex !== -1) {
e.preventDefault()
$(results[currentIndex]).removeClass('custom-selected')
$(results[currentIndex]).trigger('click')
} else if (e.key === 'Escape') {
e.preventDefault()
$(results[currentIndex]).removeClass('custom-selected')
}
})
return true
}
injectCss(String.raw`
.custom-selected {
background-color: rgba(255, 255, 255, 0.15);
}
`)
const onBodyMutations: Record<string, Callback> = {
modify_searchbox: modifySearchbox,
}
const observer = new MutationObserver((mutations, observer) => {
const results = Object.fromEntries(
Object.entries(onBodyMutations).map(([name, callback]) => {
return [name, callback(mutations, observer)]
}),
)
Object.entries(results)
.filter(([_, success]) => success)
.forEach(([name, _]) => {
delete onBodyMutations[name]
console.log(`[INFO::${name}] Success`)
})
if ($.isEmptyObject(onBodyMutations)) {
observer.disconnect()
console.log('[INFO] All observers done!')
}
})
observer.observe(document.querySelector('body')!, {
childList: true,
subtree: true,
})
And here is the copy-paste ready user script (I use webpack for bundling which is why the code contains that webpack boilerplate)
Spoiler
Code: Select all
// ==UserScript==
// @name Diablo2-IO
// @version 1.0.0
// @namespace http://tampermonkey.net/
// @description Diablo2-IO helper
// @author ideot
// @icon https://www.google.com/s2/favicons?sz=64&domain=diablo2.io
// @match https://diablo2.io/*
// @require https://cdn.jsdelivr.net/npm/dayjs@1.11.13/dayjs.min.js
// @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
// @require https://cdn.jsdelivr.net/npm/zod@3.24.2/lib/index.umd.min.js
// @require https://cdn.jsdelivr.net/npm/mousetrap@1.6.5/mousetrap.min.js
// @run-at document-body
// ==/UserScript==
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 481:
/***/ ((module) => {
module.exports = Mousetrap;
/***/ }),
/***/ 553:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
const mousetrap_1 = __importDefault(__webpack_require__(481));
const lib_1 = __webpack_require__(568);
// Focus search box when pressing
mousetrap_1.default.bind('/', () => {
$('input.topic-live-search').get(0)?.focus();
return false;
});
const getSearchResults = () => {
return $('div.acResults > ul.undefined > span.ajax_catch_two > li.ajax_link').toArray();
};
const modifySearchbox = () => {
const searchbox = $('input.topic-live-search');
if (!searchbox.length)
return false;
searchbox.on('keydown', (e) => {
const results = getSearchResults();
if (results.length === 0)
return;
const currentIndex = results.findIndex((e) => $(e).hasClass('custom-selected'));
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % results.length;
const prevIndex = currentIndex === -1 ? 0 : (currentIndex - 1 + results.length) % results.length;
if (e.key === 'ArrowDown') {
e.preventDefault();
$(results[currentIndex]).removeClass('custom-selected');
$(results[nextIndex]).addClass('custom-selected');
}
else if (e.key === 'ArrowUp') {
e.preventDefault();
$(results[currentIndex]).removeClass('custom-selected');
$(results[prevIndex]).addClass('custom-selected');
}
else if (e.key === 'Enter' && currentIndex !== -1) {
e.preventDefault();
$(results[currentIndex]).removeClass('custom-selected');
$(results[currentIndex]).trigger('click');
}
else if (e.key === 'Escape') {
e.preventDefault();
$(results[currentIndex]).removeClass('custom-selected');
}
});
return true;
};
(0, lib_1.injectCss)(String.raw `
.custom-selected {
background-color: rgba(255, 255, 255, 0.15);
}
`);
const onBodyMutations = {
modify_searchbox: modifySearchbox,
};
const observer = new MutationObserver((mutations, observer) => {
const results = Object.fromEntries(Object.entries(onBodyMutations).map(([name, callback]) => {
return [name, callback(mutations, observer)];
}));
Object.entries(results)
.filter(([_, success]) => success)
.forEach(([name, _]) => {
delete onBodyMutations[name];
console.log(`[INFO::${name}] Success`);
});
if ($.isEmptyObject(onBodyMutations)) {
observer.disconnect();
console.log('[INFO] All observers done!');
}
});
observer.observe(document.querySelector('body'), {
childList: true,
subtree: true,
});
/***/ }),
/***/ 568:
/***/ ((__unused_webpack_module, exports) => {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.injectCss = exports.loadOrStoreDefault = exports.load = exports.store = exports.countWords = exports.copyToClipboard = exports.countInstances = void 0;
const countInstances = (hay, needle) => {
let count = 0;
for (let i = hay.indexOf(needle); i !== -1 && i < hay.length; i = hay.indexOf(needle, i + 1)) {
count += 1;
}
return count;
};
exports.countInstances = countInstances;
const copyToClipboard = async (str) => {
try {
await navigator.clipboard.writeText(str);
}
catch (_err) {
alert("couldn't copy text to clipboard");
}
};
exports.copyToClipboard = copyToClipboard;
const countWords = (hay, needles) => {
return needles.reduce((acc, next) => acc + (hay.includes(next) ? 1 : 0), 0);
};
exports.countWords = countWords;
const store = (key, value) => {
window.localStorage.setItem(key, JSON.stringify(value));
};
exports.store = store;
const load = (schema, key) => {
const str = window.localStorage.getItem(key);
if (str === null)
return null;
const data = JSON.parse(str);
const result = schema.safeParse(data);
if (!result.success) {
console.error(`local storage key ${key} doesn't match schema`, result.error);
return null;
}
return result.data;
};
exports.load = load;
const loadOrStoreDefault = (schema, key, def) => {
const loaded = (0, exports.load)(schema, key);
if (loaded === null) {
(0, exports.store)(key, def);
return def;
}
return loaded;
};
exports.loadOrStoreDefault = loadOrStoreDefault;
const injectCss = (css) => {
const head = document.querySelector('head');
if (!head)
return false;
const styleElement = document.createElement('style');
styleElement.textContent = css;
head.appendChild(styleElement);
return true;
};
exports.injectCss = injectCss;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = __webpack_require__(553);
/******/
/******/ })()
;
It'd be awesome if something similar to this would get implemented for this site <3
geringverdiener
1