Search the database
Search forum topics
Search members
Search for trades
diablo2.io is supported by ads
diablo2.io is supported by ads
1 reply   486 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/[email protected]/dayjs.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/dayjs.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/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
7
User avatar

Teebling 8401Admin

Europe PC
Dude nice work. Thanks for providing source and I've added 'integrate keyboard shortcuts for search functionality' to the backlog. I will probably need to translate this to vanillaJS or jQuery as the site doesn't have provision for typescript or webpack stuff.

9

Advertisment

Hide ads
999

Greetings stranger!

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

99

Who is online

Users browsing Feedback: Etadel and 11 guests.

No matches
 

 

 

 

You haven't specified which diablo2.io user you completed this trade with. This means that you will not be able to exchange trust.

Are you sure you want to continue?

Yes, continue without username
No, I will specify a username
Choose which dclone tracking options you want to see in this widget:
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