@easyops-cn/docusaurus-search-local 0.51.1 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [0.52.0](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.51.1...v0.52.0) (2025-07-16)
6
+
7
+
8
+ ### Features
9
+
10
+ * Add configurable keyboard shortcut for search bar focus ([92b32ee](https://github.com/easyops-cn/docusaurus-search-local/commit/92b32ee0ba0ee63664b3ae2cae9aa92b433025cf)), closes [#516](https://github.com/easyops-cn/docusaurus-search-local/issues/516)
11
+
5
12
  ## [0.51.1](https://github.com/easyops-cn/docusaurus-search-local/compare/v0.51.0...v0.51.1) (2025-06-19)
6
13
 
7
14
 
@@ -9,10 +9,12 @@ import { useActivePlugin } from "@docusaurus/plugin-content-docs/client";
9
9
  import { fetchIndexesByWorker, searchByWorker } from "../searchByWorker";
10
10
  import { SuggestionTemplate } from "./SuggestionTemplate";
11
11
  import { EmptyTemplate } from "./EmptyTemplate";
12
- import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated";
12
+ import { Mark, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated";
13
13
  import LoadingRing from "../LoadingRing/LoadingRing";
14
14
  import { normalizeContextByPath } from "../../utils/normalizeContextByPath";
15
15
  import { searchResultLimits } from "../../utils/proxiedGeneratedConstants";
16
+ import { parseKeymap, matchesKeymap, getKeymapHints } from "../../utils/keymap";
17
+ import { isMacPlatform } from "../../utils/platform";
16
18
  import styles from "./SearchBar.module.css";
17
19
  async function fetchAutoCompleteJS() {
18
20
  const autoCompleteModule = await import("@easyops-cn/autocomplete.js");
@@ -291,9 +293,7 @@ export default function SearchBar({ handleSearchBarToggle, }) {
291
293
  }
292
294
  }, []);
293
295
  // Implement hint icons for the search shortcuts on mac and the rest operating systems.
294
- const isMac = isBrowser
295
- ? /mac/i.test(navigator.userAgentData?.platform ?? navigator.platform)
296
- : false;
296
+ const isMac = isBrowser ? isMacPlatform() : false;
297
297
  // Sync the input value and focus state for SSR
298
298
  useEffect(() => {
299
299
  const searchBar = searchBarRef.current;
@@ -312,13 +312,13 @@ export default function SearchBar({ handleSearchBarToggle, }) {
312
312
  // eslint-disable-next-line react-hooks/exhaustive-deps
313
313
  []);
314
314
  useEffect(() => {
315
- if (!searchBarShortcut) {
315
+ if (!searchBarShortcut || !searchBarShortcutKeymap) {
316
316
  return;
317
317
  }
318
- // Add shortcuts command/ctrl + K
318
+ const parsedKeymap = parseKeymap(searchBarShortcutKeymap);
319
+ // Add shortcuts based on custom keymap
319
320
  const handleShortcut = (event) => {
320
- if ((isMac ? event.metaKey : event.ctrlKey) &&
321
- (event.key === "k" || event.key === "K")) {
321
+ if (matchesKeymap(event, parsedKeymap)) {
322
322
  event.preventDefault();
323
323
  searchBarRef.current?.focus();
324
324
  onInputFocus();
@@ -328,7 +328,7 @@ export default function SearchBar({ handleSearchBarToggle, }) {
328
328
  return () => {
329
329
  document.removeEventListener("keydown", handleShortcut);
330
330
  };
331
- }, [isMac, onInputFocus]);
331
+ }, [onInputFocus, searchBarShortcutKeymap]);
332
332
  const onClearSearch = useCallback(() => {
333
333
  const params = new URLSearchParams(location.search);
334
334
  params.delete(SEARCH_PARAM_HIGHLIGHT);
@@ -359,9 +359,8 @@ export default function SearchBar({ handleSearchBarToggle, }) {
359
359
  searchBarShortcutHint &&
360
360
  (inputValue !== "" ? (<button className={styles.searchClearButton} onClick={onClearSearch}>
361
361
 
362
- </button>) : (isBrowser && (<div className={styles.searchHintContainer}>
363
- <kbd className={styles.searchHint}>{isMac ? "⌘" : "ctrl"}</kbd>
364
- <kbd className={styles.searchHint}>K</kbd>
362
+ </button>) : (isBrowser && searchBarShortcutKeymap && (<div className={styles.searchHintContainer}>
363
+ {getKeymapHints(searchBarShortcutKeymap, isMac).map((hint, index) => (<kbd key={index} className={styles.searchHint}>{hint}</kbd>))}
365
364
  </div>)))}
366
365
  </div>);
367
366
  }
@@ -0,0 +1,75 @@
1
+ import { isMacPlatform } from './platform';
2
+ export function parseKeymap(keymap) {
3
+ const parts = keymap.toLowerCase().split('+');
4
+ const result = {
5
+ key: '',
6
+ ctrl: false,
7
+ alt: false,
8
+ shift: false,
9
+ meta: false,
10
+ };
11
+ // Detect if we're on Mac to handle 'mod' appropriately
12
+ const isMac = isMacPlatform();
13
+ for (const part of parts) {
14
+ const trimmed = part.trim();
15
+ switch (trimmed) {
16
+ case 'ctrl':
17
+ result.ctrl = true;
18
+ break;
19
+ case 'cmd':
20
+ result.meta = true;
21
+ break;
22
+ case 'mod':
23
+ if (isMac) {
24
+ result.meta = true;
25
+ }
26
+ else {
27
+ result.ctrl = true;
28
+ }
29
+ break;
30
+ case 'alt':
31
+ result.alt = true;
32
+ break;
33
+ case 'shift':
34
+ result.shift = true;
35
+ break;
36
+ default:
37
+ result.key = trimmed;
38
+ break;
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ export function matchesKeymap(event, keymap) {
44
+ return (event.key.toLowerCase() === keymap.key &&
45
+ event.ctrlKey === keymap.ctrl &&
46
+ event.altKey === keymap.alt &&
47
+ event.shiftKey === keymap.shift &&
48
+ event.metaKey === keymap.meta);
49
+ }
50
+ export function getKeymapHints(keymap, isMac) {
51
+ const parsedKeymap = parseKeymap(keymap);
52
+ const hints = [];
53
+ // Handle original keymap string to detect 'mod' for proper hint display
54
+ const parts = keymap.toLowerCase().split('+').map(p => p.trim());
55
+ const hasMod = parts.includes('mod');
56
+ if (parsedKeymap.ctrl && !hasMod) {
57
+ hints.push('ctrl');
58
+ }
59
+ if (parsedKeymap.meta && !hasMod) {
60
+ hints.push(isMac ? '⌘' : 'cmd');
61
+ }
62
+ if (hasMod) {
63
+ hints.push(isMac ? '⌘' : 'ctrl');
64
+ }
65
+ if (parsedKeymap.alt) {
66
+ hints.push(isMac ? '⌥' : 'alt');
67
+ }
68
+ if (parsedKeymap.shift) {
69
+ hints.push(isMac ? '⇧' : 'shift');
70
+ }
71
+ if (parsedKeymap.key) {
72
+ hints.push(parsedKeymap.key.toUpperCase());
73
+ }
74
+ return hints;
75
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Detects if the current platform is macOS using modern, non-deprecated APIs.
3
+ * Falls back gracefully for older browsers or server-side rendering.
4
+ */
5
+ export function isMacPlatform() {
6
+ // Handle server-side rendering or missing navigator
7
+ if (typeof navigator === 'undefined') {
8
+ return false;
9
+ }
10
+ // Try modern User-Agent Client Hints API first (if available)
11
+ if ('userAgentData' in navigator && navigator.userAgentData?.platform) {
12
+ const platform = navigator.userAgentData.platform.toLowerCase();
13
+ return platform.includes('mac');
14
+ }
15
+ // Fall back to user agent string parsing (more reliable than navigator.platform)
16
+ if (navigator.userAgent) {
17
+ return /mac/i.test(navigator.userAgent);
18
+ }
19
+ // Final fallback to deprecated navigator.platform for very old browsers
20
+ if (navigator.platform) {
21
+ return navigator.platform.toLowerCase().includes('mac');
22
+ }
23
+ return false;
24
+ }
@@ -6,7 +6,7 @@ const fs_1 = tslib_1.__importDefault(require("fs"));
6
6
  const path_1 = tslib_1.__importDefault(require("path"));
7
7
  const getIndexHash_1 = require("./getIndexHash");
8
8
  function generate(config, dir) {
9
- const { language, removeDefaultStopWordFilter, removeDefaultStemmer, highlightSearchTermsOnTargetPage, searchResultLimits, searchResultContextMaxLength, explicitSearchResultPath, searchBarShortcut, searchBarShortcutHint, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, fuzzyMatchingDistance, } = config;
9
+ const { language, removeDefaultStopWordFilter, removeDefaultStemmer, highlightSearchTermsOnTargetPage, searchResultLimits, searchResultContextMaxLength, explicitSearchResultPath, searchBarShortcut, searchBarShortcutHint, searchBarShortcutKeymap, searchBarPosition, docsPluginIdForPreferredVersion, indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, useAllContextsWithNoSearchContext, fuzzyMatchingDistance, } = config;
10
10
  const indexHash = (0, getIndexHash_1.getIndexHash)(config);
11
11
  const contents = [];
12
12
  contents.push(`export const removeDefaultStemmer = ${JSON.stringify(removeDefaultStemmer)};`);
@@ -30,6 +30,7 @@ function generate(config, dir) {
30
30
  contents.push(`export const explicitSearchResultPath = ${JSON.stringify(explicitSearchResultPath)};`);
31
31
  contents.push(`export const searchBarShortcut = ${JSON.stringify(searchBarShortcut)};`);
32
32
  contents.push(`export const searchBarShortcutHint = ${JSON.stringify(searchBarShortcutHint)};`);
33
+ contents.push(`export const searchBarShortcutKeymap = ${JSON.stringify(searchBarShortcutKeymap)};`);
33
34
  contents.push(`export const searchBarPosition = ${JSON.stringify(searchBarPosition)};`);
34
35
  contents.push(`export const docsPluginIdForPreferredVersion = ${docsPluginIdForPreferredVersion === undefined
35
36
  ? "undefined"
@@ -27,6 +27,7 @@ const schema = utils_validation_1.Joi.object({
27
27
  ignoreCssSelectors: isStringOrArrayOfStrings.default([]),
28
28
  searchBarShortcut: utils_validation_1.Joi.boolean().default(true),
29
29
  searchBarShortcutHint: utils_validation_1.Joi.boolean().default(true),
30
+ searchBarShortcutKeymap: utils_validation_1.Joi.string().default("mod+k"),
30
31
  searchBarPosition: utils_validation_1.Joi.string().default("auto"),
31
32
  docsPluginIdForPreferredVersion: utils_validation_1.Joi.string(),
32
33
  zhUserDict: utils_validation_1.Joi.string(),
@@ -124,6 +124,17 @@ export interface PluginOptions {
124
124
  * @default true
125
125
  */
126
126
  searchBarShortcutHint?: boolean;
127
+ /**
128
+ * Custom keyboard shortcut to focus the search bar. Supports formats like:
129
+ * - "s" for single key
130
+ * - "ctrl+k" for key combinations
131
+ * - "cmd+k" for Command+K (Mac) / Ctrl+K (others)
132
+ * - "mod+k" for Command+K (Mac) / Ctrl+K (others) - recommended cross-platform option
133
+ * - "ctrl+shift+k" for multiple modifiers
134
+ *
135
+ * @default "mod+k"
136
+ */
137
+ searchBarShortcutKeymap?: string;
127
138
  /**
128
139
  * The side of the navbar the search bar should appear on. By default,
129
140
  * it will try to autodetect based on your docusaurus config according
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyops-cn/docusaurus-search-local",
3
- "version": "0.51.1",
3
+ "version": "0.52.0",
4
4
  "description": "An offline/local search plugin for Docusaurus v3",
5
5
  "repository": {
6
6
  "type": "git",