Search the database
Search forum topics
Search members
Search for trades
diablo2.io is supported by ads
diablo2.io is supported by ads
0 replies   99 views
1

Keyboard shortcuts for the search function

No data yet

Site Feedback

2

Description

Hi,

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
Here's the raw source code (TypeScript), although it's just a proof of concept that does enough for me
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
5

Can be used to make Runewords:

7
Hi,

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
Here's the raw source code (TypeScript), although it's just a proof of concept that does enough for me
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
Last bumped by geringverdiener 1 week ago.
9

Advertisment

Hide ads
999

Greetings stranger!

You don't appear to be logged in...

99

Who is online

Users browsing Feedback: No registered users and 1 guest.

999

Register an account


Start trading, earning trust, and levelling up. Get avatars, likes, bookmarks and more with an account! :)
999

Support Diablo2.io


You can donate to the site to help support the future of diablo2.io. Donating hides all ads forever.
999

Latest discussion




999

Found a bug or glitch?


If something looks broken please let me know so I can fix it :)
No matches
 

 

 

 

Value:
Hide ads forever by supporting the site with a donation.

Greetings adblocker...

Warriv asks that you consider disabling your adblocker when using diablo2.io

Ad revenue helps keep the servers going and supports me, the site's creator :)

A one-time donation hides all ads, forever:
Make a donation