From 582ea8bd307ac04150a35573848249675676b00d Mon Sep 17 00:00:00 2001 From: "Michael E. Karpeles" Date: Thu, 4 Jun 2026 14:07:48 -0600 Subject: [PATCH 1/4] feat: open search panel on load when ?q= param is present but empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ?q= is in the URL, BookReader already searches on load. When ?q= is present but empty, it now opens the search panel without running a search — making search-inside discoverable by default. Two changes: - BookReader.js: use `!= null` instead of truthiness check so that an empty string from URLSearchParams.get('q') passes through to initialSearchTerm (previously empty ?q= was silently ignored) - plugin.search.js: add else-if branch for initialSearchTerm === '' that calls toggleSidebar() to open the panel without triggering a search Closes: internetarchive/openlibrary#11912 Related: #1166 --- src/BookReader.js | 2 +- src/plugins/search/plugin.search.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/BookReader.js b/src/BookReader.js index 7ddcb4834..29b02a401 100644 --- a/src/BookReader.js +++ b/src/BookReader.js @@ -541,7 +541,7 @@ BookReader.prototype.initParams = function() { // If we have a query string: q=[term] const searchParams = new URLSearchParams(this.readQueryString()); const searchTerm = searchParams.get('q'); - if (searchTerm) { + if (searchTerm != null) { sp.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm); } } diff --git a/src/plugins/search/plugin.search.js b/src/plugins/search/plugin.search.js index 17613376d..fc6376b38 100644 --- a/src/plugins/search/plugin.search.js +++ b/src/plugins/search/plugin.search.js @@ -104,6 +104,9 @@ export class SearchPlugin extends BookReaderPlugin { this.options.initialSearchTerm, { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false }, ); + } else if (this.options.initialSearchTerm === '') { + // ?q= present but empty: open the search panel without running a search + this.searchView.toggleSidebar(); } } From 8488994e19d00bd33ce610f14109a09ee4cec45a Mon Sep 17 00:00:00 2001 From: "Michael E. Karpeles" Date: Thu, 4 Jun 2026 14:31:11 -0600 Subject: [PATCH 2/4] test: add unit test for empty initialSearchTerm opening search panel --- tests/jest/plugins/search/plugin.search.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/jest/plugins/search/plugin.search.test.js b/tests/jest/plugins/search/plugin.search.test.js index 87ffaadde..8474e0480 100644 --- a/tests/jest/plugins/search/plugin.search.test.js +++ b/tests/jest/plugins/search/plugin.search.test.js @@ -60,6 +60,15 @@ describe('Plugin: Search', () => { .toHaveProperty('suppressFragmentChange', false); }); + test('On init with empty initialSearchTerm (""), it opens the search panel without searching', () => { + br.plugins.search.search = jest.fn(); + br.options.plugins.search.initialSearchTerm = ''; + br.init(); + + expect(br.plugins.search.searchView.toggleSidebar).toHaveBeenCalled(); + expect(br.plugins.search.search).not.toHaveBeenCalled(); + }); + test('calling `search` fires ajax call`', () => { br.init(); br.search('foo'); From e8b912f0b3158cb075266d6954a5f7d92a001015 Mon Sep 17 00:00:00 2001 From: "Michael E. Karpeles" Date: Fri, 5 Jun 2026 17:37:10 -0600 Subject: [PATCH 3/4] fix: pass null (not '') as initialSearchTerm default in IADemoBr.js Without ?q= in the URL, urlParams.get('q') returns null. The previous ternary `searchTerm ? searchTerm : ''` converted null to '' which caused the new `else if (initialSearchTerm === '')` branch in plugin.search.js to fire toggleSidebar() on every page load, opening the search panel unexpectedly and breaking the e2e tests. --- BookReaderDemo/IADemoBr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BookReaderDemo/IADemoBr.js b/BookReaderDemo/IADemoBr.js index 4d741f74f..a350c4cd3 100644 --- a/BookReaderDemo/IADemoBr.js +++ b/BookReaderDemo/IADemoBr.js @@ -85,7 +85,7 @@ const initializeBookReader = (brManifest) => { enableFSLogoShortcut: true, plugins: { search: { - initialSearchTerm: searchTerm ? searchTerm : '', + initialSearchTerm: searchTerm, }, }, }; From 21c0fe1632e044137d8b972f5006ff267c7c5485 Mon Sep 17 00:00:00 2001 From: Drini Cami Date: Thu, 2 Jul 2026 16:16:02 +0000 Subject: [PATCH 4/4] feat: open search panel on load when ?focus=search is in URL Co-Authored-By: Claude Sonnet 4.6 --- src/BookReader.js | 7 ++++++- src/plugins/search/plugin.search.js | 3 +-- src/plugins/url/UrlPlugin.js | 12 ++++++++++++ tests/jest/plugins/search/plugin.search.test.js | 4 ++-- tests/jest/plugins/url/plugin.url.test.js | 1 + 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/BookReader.js b/src/BookReader.js index 29b02a401..dd8704a65 100644 --- a/src/BookReader.js +++ b/src/BookReader.js @@ -541,7 +541,7 @@ BookReader.prototype.initParams = function() { // If we have a query string: q=[term] const searchParams = new URLSearchParams(this.readQueryString()); const searchTerm = searchParams.get('q'); - if (searchTerm != null) { + if (searchTerm) { sp.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm); } } @@ -645,6 +645,7 @@ BookReader.prototype.init = function() { this.pageScale = this.reduce; // preserve current reduce const params = this.initParams(); + this.urlPlugin?.pullFromAddressBar(); this.firstIndex = params.index ? params.index : 0; // Setup Navbars and other UI @@ -1960,6 +1961,10 @@ BookReader.prototype.queryStringFromParams = function( ) { const newParams = new URLSearchParams(currQueryString); + // Never write back UI-only params that are consumed on load + // TODO: Should ideally stick around until next URL change + newParams.delete('focus'); + if (params.view) { // Set ?view=theater when fullscreen newParams.set('view', params.view); diff --git a/src/plugins/search/plugin.search.js b/src/plugins/search/plugin.search.js index fc6376b38..c6ba05757 100644 --- a/src/plugins/search/plugin.search.js +++ b/src/plugins/search/plugin.search.js @@ -104,8 +104,7 @@ export class SearchPlugin extends BookReaderPlugin { this.options.initialSearchTerm, { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false }, ); - } else if (this.options.initialSearchTerm === '') { - // ?q= present but empty: open the search panel without running a search + } else if (this.br.urlPlugin?.getUrlParam('focus') === 'search') { this.searchView.toggleSidebar(); } } diff --git a/src/plugins/url/UrlPlugin.js b/src/plugins/url/UrlPlugin.js index 5425cf6b2..268bb5d18 100644 --- a/src/plugins/url/UrlPlugin.js +++ b/src/plugins/url/UrlPlugin.js @@ -1,8 +1,18 @@ +/** + * @typedef {Object} UrlSchemaEntry + * @property {string} name + * @property {'path' | 'query_param'} position + * @property {string} [default] + * @property {string} [deprecated_for] + * @property {boolean} [readOnly] - If true, the param is read from the URL but never written back + */ + export class UrlPlugin { constructor(options = {}) { this.bookReaderOptions = options; // the canonical order of elements is important in the path and query string + /** @type {UrlSchemaEntry[]} */ this.urlSchema = [ { name: 'page', position: 'path', default: 'n0' }, { name: 'mode', position: 'path', default: '2up' }, @@ -11,6 +21,7 @@ export class UrlPlugin { { name: 'sort', position: 'query_param' }, { name: 'view', position: 'query_param' }, { name: 'admin', position: 'query_param' }, + { name: 'focus', position: 'query_param', readOnly: true }, ]; this.urlState = {}; @@ -36,6 +47,7 @@ export class UrlPlugin { if (schema?.deprecated_for) { schema = this.urlSchema.find(schemaKey => schemaKey.name === schema.deprecated_for); } + if (schema?.readOnly) return; if (schema?.position == 'path') { pathParams[schema?.name] = urlState[key]; } else { diff --git a/tests/jest/plugins/search/plugin.search.test.js b/tests/jest/plugins/search/plugin.search.test.js index 8474e0480..d5f4f2f2a 100644 --- a/tests/jest/plugins/search/plugin.search.test.js +++ b/tests/jest/plugins/search/plugin.search.test.js @@ -60,9 +60,9 @@ describe('Plugin: Search', () => { .toHaveProperty('suppressFragmentChange', false); }); - test('On init with empty initialSearchTerm (""), it opens the search panel without searching', () => { + test('On init with #focus=search in URL hash, it opens the search panel without searching', () => { br.plugins.search.search = jest.fn(); - br.options.plugins.search.initialSearchTerm = ''; + br.urlPlugin = { pullFromAddressBar: jest.fn(), getUrlParam: jest.fn(() => 'search') }; br.init(); expect(br.plugins.search.searchView.toggleSidebar).toHaveBeenCalled(); diff --git a/tests/jest/plugins/url/plugin.url.test.js b/tests/jest/plugins/url/plugin.url.test.js index 4d8139fc1..9807ee46c 100644 --- a/tests/jest/plugins/url/plugin.url.test.js +++ b/tests/jest/plugins/url/plugin.url.test.js @@ -7,6 +7,7 @@ beforeAll(() => { document.body.innerHTML = '
'; br = new BookReader(); const urlPluginMock = { + pullFromAddressBar: sinon.fake(), retrieveTextFragment: sinon.fake(), urlStringToUrlState: sinon.fake(), parseToText: sinon.fake(),