@internetarchive/bookreader 5.0.0-91 → 5.0.0-92
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/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +2 -2
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.chapters.js +2 -2
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.iiif.js +1 -1
- package/BookReader/plugins/plugin.iiif.js.map +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.resume.js.map +1 -1
- package/BookReader/plugins/plugin.search.js +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +1 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReaderDemo/IADemoBr.js +29 -1
- package/BookReaderDemo/ia-multiple-volumes-manifest.js +0 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/BookNavigator/book-navigator.js +5 -2
- package/src/BookNavigator/search/search-provider.js +13 -7
- package/src/BookNavigator/sharing.js +1 -1
- package/src/BookReader/Toolbar/Toolbar.js +5 -0
- package/src/BookReader/options.js +9 -7
- package/src/BookReader.js +31 -15
- package/src/BookReaderPlugin.js +8 -0
- package/src/plugins/plugin.text_selection.js +3 -1
- package/src/plugins/search/plugin.search.js +330 -376
- package/src/plugins/search/view.js +13 -9
- package/tests/e2e/helpers/mockSearch.js +1 -1
- package/tests/jest/BookNavigator/book-navigator.test.js +8 -3
- package/tests/jest/BookNavigator/search/search-provider.test.js +16 -4
- package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
- package/tests/jest/BookReader.test.js +26 -1
- package/tests/jest/plugins/search/plugin.search.test.js +17 -42
- package/tests/jest/plugins/search/plugin.search.view.test.js +10 -18
- package/tests/jest/plugins/url/plugin.url.test.js +1 -1
@@ -1,5 +1,4 @@
|
|
1
1
|
// @ts-check
|
2
|
-
/* global BookReader */
|
3
2
|
/**
|
4
3
|
* Plugin for Archive.org book search
|
5
4
|
* Events fired at various points throughout search processing are published
|
@@ -25,66 +24,70 @@ import { poll } from '../../BookReader/utils.js';
|
|
25
24
|
import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
|
26
25
|
import SearchView from './view.js';
|
27
26
|
import { marshallSearchResults } from './utils.js';
|
27
|
+
import { BookReaderPlugin } from '../../BookReaderPlugin.js';
|
28
|
+
import { applyVariables } from '../../util/strings.js';
|
28
29
|
/** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
|
29
30
|
/** @typedef {import('../../BookReader/BookModel').PageIndex} PageIndex */
|
30
31
|
/** @typedef {import('../../BookReader/BookModel').LeafNum} LeafNum */
|
31
32
|
/** @typedef {import('../../BookReader/BookModel').PageNumString} PageNumString */
|
32
33
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
34
|
+
// @ts-ignore
|
35
|
+
const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
|
36
|
+
|
37
|
+
export class SearchPlugin extends BookReaderPlugin {
|
38
|
+
options = {
|
39
|
+
enabled: true,
|
40
|
+
preTag: '{{{',
|
41
|
+
postTag: '}}}',
|
42
|
+
/**
|
43
|
+
* @type {import('@/src/util/strings.js').StringWithVars}
|
44
|
+
* Provides the variables: `query`, `preTag`, and `postTag` (from these options)
|
45
|
+
*/
|
46
|
+
searchInsideUrl: '/fulltext/inside.php?' + [
|
47
|
+
'q={{query|urlencode}}',
|
48
|
+
'pre_tag={{preTag|urlencode}}',
|
49
|
+
'post_tag={{postTag|urlencode}}',
|
50
|
+
].join('&'),
|
51
|
+
/** @type {string} */
|
52
|
+
initialSearchTerm: null,
|
53
|
+
goToFirstResult: false,
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* @override
|
58
|
+
* @param {Partial<SearchPlugin['options']>} options
|
59
|
+
**/
|
60
|
+
setup(options) {
|
61
|
+
super.setup(options);
|
50
62
|
|
51
63
|
this.searchTerm = '';
|
64
|
+
/** @type {SearchInsideResults | null} */
|
52
65
|
this.searchResults = null;
|
53
|
-
this.searchInsideUrl = options.searchInsideUrl;
|
54
|
-
this.
|
55
|
-
|
56
|
-
// Base server used by some api calls
|
57
|
-
this.bookId = options.bookId;
|
58
|
-
this.server = options.server;
|
59
|
-
this.subPrefix = options.subPrefix;
|
60
|
-
this.bookPath = options.bookPath;
|
61
|
-
|
66
|
+
this.searchInsideUrl = this.options.searchInsideUrl;
|
67
|
+
this.enabled = this.options.enabled;
|
62
68
|
this.searchXHR = null;
|
63
|
-
this._cancelSearch.bind(this);
|
64
|
-
this.cancelSearchRequest.bind(this);
|
65
69
|
|
66
70
|
/** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
|
67
71
|
this._searchBoxesByIndex = {};
|
68
72
|
|
69
|
-
if (this.
|
73
|
+
if (this.enabled) {
|
70
74
|
this.searchView = new SearchView({
|
71
|
-
br: this,
|
75
|
+
br: this.br,
|
72
76
|
searchCancelledCallback: () => {
|
73
77
|
this._cancelSearch();
|
74
|
-
this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
|
78
|
+
this.br.trigger('SearchCanceled', { term: this.searchTerm, instance: this.br });
|
75
79
|
},
|
76
80
|
});
|
77
81
|
} else {
|
78
82
|
this.searchView = null;
|
79
83
|
}
|
80
|
-
}
|
81
|
-
|
84
|
+
}
|
85
|
+
|
86
|
+
/** @override */
|
87
|
+
init() {
|
88
|
+
super.init();
|
82
89
|
|
83
|
-
|
84
|
-
BookReader.prototype.init = (function (super_) {
|
85
|
-
return function () {
|
86
|
-
super_.call(this);
|
87
|
-
if (!this.enableSearch) return;
|
90
|
+
if (!this.enabled) return;
|
88
91
|
|
89
92
|
this.searchView.init();
|
90
93
|
if (this.options.initialSearchTerm) {
|
@@ -101,26 +104,27 @@ BookReader.prototype.init = (function (super_) {
|
|
101
104
|
{ goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false },
|
102
105
|
);
|
103
106
|
}
|
104
|
-
}
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
107
|
+
}
|
108
|
+
|
109
|
+
/**
|
110
|
+
* @override
|
111
|
+
* @protected
|
112
|
+
* @param {JQuery<HTMLElement>} $toolbarElement
|
113
|
+
*/
|
114
|
+
_configureToolbar($toolbarElement) {
|
115
|
+
if (!this.enabled) { return; }
|
112
116
|
if (this.searchView.dom.toolbarSearch) {
|
113
|
-
$
|
117
|
+
$toolbarElement.find('.BRtoolbarSectionInfo').after(this.searchView.dom.toolbarSearch);
|
114
118
|
}
|
115
|
-
return $
|
116
|
-
}
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
if (this.
|
119
|
+
return $toolbarElement;
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* @override
|
124
|
+
* @param {import ("@/src/BookReader/PageContainer.js").PageContainer} pageContainer
|
125
|
+
*/
|
126
|
+
_configurePageContainer(pageContainer) {
|
127
|
+
if (this.enabled && pageContainer.page && pageContainer.page.index in this._searchBoxesByIndex) {
|
124
128
|
const pageIndex = pageContainer.page.index;
|
125
129
|
const boxes = this._searchBoxesByIndex[pageIndex];
|
126
130
|
renderBoxesInPageContainerLayer(
|
@@ -132,362 +136,312 @@ BookReader.prototype._createPageContainer = (function (super_) {
|
|
132
136
|
);
|
133
137
|
}
|
134
138
|
return pageContainer;
|
135
|
-
};
|
136
|
-
})(BookReader.prototype._createPageContainer);
|
137
|
-
|
138
|
-
/**
|
139
|
-
* @typedef {object} SearchOptions
|
140
|
-
* @property {boolean} goToFirstResult
|
141
|
-
* @property {boolean} disablePopup
|
142
|
-
* @property {(null|function)} error - @deprecated at v.5.0
|
143
|
-
* @property {(null|function)} success - @deprecated at v.5.0
|
144
|
-
*/
|
145
|
-
|
146
|
-
/**
|
147
|
-
* Submits search request
|
148
|
-
*
|
149
|
-
* @param {string} term
|
150
|
-
* @param {SearchOptions} overrides
|
151
|
-
*/
|
152
|
-
BookReader.prototype.search = async function(term = '', overrides = {}) {
|
153
|
-
/** @type {SearchOptions} */
|
154
|
-
const defaultOptions = {
|
155
|
-
goToFirstResult: false, /* jump to the first result (default=false) */
|
156
|
-
disablePopup: false, /* don't show the modal progress (default=false) */
|
157
|
-
suppressFragmentChange: false, /* don't change the URL on initial load */
|
158
|
-
error: null, /* optional error handler (default=null) */
|
159
|
-
success: null, /* optional success handler (default=null) */
|
160
|
-
|
161
|
-
};
|
162
|
-
const options = jQuery.extend({}, defaultOptions, overrides);
|
163
|
-
this.suppressFragmentChange = options.suppressFragmentChange;
|
164
|
-
this.searchCancelled = false;
|
165
|
-
|
166
|
-
// strip slashes, since this goes in the url
|
167
|
-
this.searchTerm = term.replace(/\//g, ' ');
|
168
|
-
|
169
|
-
if (!options.suppressFragmentChange) {
|
170
|
-
this.trigger(BookReader.eventNames.fragmentChange);
|
171
139
|
}
|
172
140
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
141
|
+
/**
|
142
|
+
* Submits search request
|
143
|
+
*
|
144
|
+
* @param {string} term
|
145
|
+
* @param {Partial<SearchOptions>} overrides
|
146
|
+
*/
|
147
|
+
async search(term = '', overrides = {}) {
|
148
|
+
/** @type {SearchOptions} */
|
149
|
+
const defaultOptions = {
|
150
|
+
goToFirstResult: false, /* jump to the first result (default=false) */
|
151
|
+
disablePopup: false, /* don't show the modal progress (default=false) */
|
152
|
+
suppressFragmentChange: false, /* don't change the URL on initial load */
|
153
|
+
error: null, /* optional error handler (default=null) */
|
154
|
+
success: null, /* optional success handler (default=null) */
|
155
|
+
|
156
|
+
};
|
157
|
+
const options = jQuery.extend({}, defaultOptions, overrides);
|
158
|
+
this.suppressFragmentChange = options.suppressFragmentChange;
|
159
|
+
this.searchCancelled = false;
|
160
|
+
|
161
|
+
// strip slashes, since this goes in the url
|
162
|
+
this.searchTerm = term.replace(/\//g, ' ');
|
163
|
+
|
164
|
+
if (!options.suppressFragmentChange) {
|
165
|
+
this.br.trigger(BookReader.eventNames.fragmentChange);
|
166
|
+
}
|
180
167
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
if (this.bookPath.length - this.bookPath.lastIndexOf(subPrefixWithSlash) == subPrefixWithSlash.length) {
|
185
|
-
path = this.bookPath.substr(0, this.bookPath.length - subPrefixWithSlash.length);
|
186
|
-
}
|
168
|
+
// Add quotes to the term. This is to compenstate for the backends default OR query
|
169
|
+
// term = term.replace(/['"]+/g, '');
|
170
|
+
// term = '"' + term + '"';
|
187
171
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
pre_tag: this.options.searchInsidePreTag,
|
194
|
-
post_tag: this.options.searchInsidePostTag,
|
195
|
-
};
|
172
|
+
const url = applyVariables(this.options.searchInsideUrl, this.br.options.vars, {
|
173
|
+
query: term,
|
174
|
+
preTag: this.options.preTag,
|
175
|
+
postTag: this.options.postTag,
|
176
|
+
});
|
196
177
|
|
197
|
-
|
198
|
-
|
178
|
+
const callSearchResultsCallback = (searchInsideResults) => {
|
179
|
+
if (this.searchCancelled) {
|
180
|
+
return;
|
181
|
+
}
|
182
|
+
const responseHasError = searchInsideResults.error || !searchInsideResults.matches.length;
|
183
|
+
const hasCustomError = typeof options.error === 'function';
|
184
|
+
const hasCustomSuccess = typeof options.success === 'function';
|
185
|
+
|
186
|
+
if (responseHasError) {
|
187
|
+
console.error('Search Inside Response Error', searchInsideResults.error || 'matches.length == 0');
|
188
|
+
hasCustomError
|
189
|
+
? options.error.call(this, searchInsideResults, options)
|
190
|
+
: this.BRSearchCallbackError(searchInsideResults);
|
191
|
+
} else {
|
192
|
+
hasCustomSuccess
|
193
|
+
? options.success.call(this, searchInsideResults, options)
|
194
|
+
: this.BRSearchCallback(searchInsideResults, options);
|
195
|
+
}
|
196
|
+
};
|
197
|
+
|
198
|
+
this.br.trigger('SearchStarted', { term: this.searchTerm, instance: this.br });
|
199
|
+
callSearchResultsCallback(await $.ajax({
|
200
|
+
url: url,
|
201
|
+
cache: true,
|
202
|
+
xhrFields: {
|
203
|
+
withCredentials: this.br.protected,
|
204
|
+
},
|
205
|
+
beforeSend: xhr => { this.searchXHR = xhr; },
|
206
|
+
}));
|
207
|
+
}
|
199
208
|
|
200
|
-
|
209
|
+
/**
|
210
|
+
* cancels AJAX Call
|
211
|
+
* emits custom event
|
212
|
+
*/
|
213
|
+
_cancelSearch() {
|
214
|
+
this.searchXHR?.abort();
|
215
|
+
this.searchView.clearSearchFieldAndResults(false);
|
216
|
+
this.searchTerm = '';
|
217
|
+
this.searchXHR = null;
|
218
|
+
this.searchCancelled = true;
|
219
|
+
this.searchResults = null;
|
220
|
+
}
|
201
221
|
|
202
|
-
|
203
|
-
|
204
|
-
|
222
|
+
/**
|
223
|
+
* External function to cancel search
|
224
|
+
* checks for term & xhr in flight before running
|
225
|
+
*/
|
226
|
+
cancelSearchRequest() {
|
227
|
+
this.searchCancelled = true;
|
228
|
+
if (this.searchXHR !== null) {
|
229
|
+
this._cancelSearch();
|
230
|
+
this.searchView.toggleSearchPending();
|
231
|
+
this.br.trigger('SearchCanceled', { term: this.searchTerm, instance: this.br });
|
205
232
|
}
|
206
|
-
const responseHasError = searchInsideResults.error || !searchInsideResults.matches.length;
|
207
|
-
const hasCustomError = typeof options.error === 'function';
|
208
|
-
const hasCustomSuccess = typeof options.success === 'function';
|
209
|
-
|
210
|
-
if (responseHasError) {
|
211
|
-
console.error('Search Inside Response Error', searchInsideResults.error || 'matches.length == 0');
|
212
|
-
hasCustomError
|
213
|
-
? options.error.call(this, searchInsideResults, options)
|
214
|
-
: this.BRSearchCallbackError(searchInsideResults, options);
|
215
|
-
} else {
|
216
|
-
hasCustomSuccess
|
217
|
-
? options.success.call(this, searchInsideResults, options)
|
218
|
-
: this.BRSearchCallback(searchInsideResults, options);
|
219
|
-
}
|
220
|
-
};
|
221
|
-
|
222
|
-
this.trigger('SearchStarted', { term: this.searchTerm, instance: this });
|
223
|
-
callSearchResultsCallback(await $.ajax({
|
224
|
-
url: url,
|
225
|
-
cache: true,
|
226
|
-
xhrFields: {
|
227
|
-
withCredentials: this.protected,
|
228
|
-
},
|
229
|
-
beforeSend: xhr => { this.searchXHR = xhr; },
|
230
|
-
}));
|
231
|
-
};
|
232
|
-
|
233
|
-
/**
|
234
|
-
* cancels AJAX Call
|
235
|
-
* emits custom event
|
236
|
-
*/
|
237
|
-
BookReader.prototype._cancelSearch = function () {
|
238
|
-
this.searchXHR?.abort();
|
239
|
-
this.searchView.clearSearchFieldAndResults(false);
|
240
|
-
this.searchTerm = '';
|
241
|
-
this.searchXHR = null;
|
242
|
-
this.searchCancelled = true;
|
243
|
-
this.searchResults = [];
|
244
|
-
};
|
245
|
-
|
246
|
-
/**
|
247
|
-
* External function to cancel search
|
248
|
-
* checks for term & xhr in flight before running
|
249
|
-
*/
|
250
|
-
BookReader.prototype.cancelSearchRequest = function () {
|
251
|
-
this.searchCancelled = true;
|
252
|
-
if (this.searchXHR !== null) {
|
253
|
-
this._cancelSearch();
|
254
|
-
this.searchView.toggleSearchPending();
|
255
|
-
this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
|
256
233
|
}
|
257
|
-
};
|
258
234
|
|
259
|
-
/**
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
235
|
+
/**
|
236
|
+
* Search Results return handler
|
237
|
+
* @param {SearchInsideResults} results
|
238
|
+
* @param {object} options
|
239
|
+
* @param {boolean} options.goToFirstResult
|
240
|
+
*/
|
241
|
+
BRSearchCallback(results, options) {
|
242
|
+
marshallSearchResults(
|
243
|
+
results,
|
244
|
+
pageNum => this.br.book.getPageNum(this.br.book.leafNumToIndex(pageNum)),
|
245
|
+
this.options.preTag,
|
246
|
+
this.options.postTag,
|
247
|
+
);
|
248
|
+
this.searchResults = results || null;
|
249
|
+
|
250
|
+
this.updateSearchHilites();
|
251
|
+
this.br.removeProgressPopup();
|
252
|
+
if (options.goToFirstResult) {
|
253
|
+
this.jumpToMatch(0);
|
254
|
+
}
|
255
|
+
this.br.trigger('SearchCallback', { results, options, instance: this.br });
|
256
|
+
}
|
278
257
|
|
279
|
-
/**
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
258
|
+
/**
|
259
|
+
* Main search results error handler
|
260
|
+
* @param {SearchInsideResults} results
|
261
|
+
*/
|
262
|
+
BRSearchCallbackError(results) {
|
263
|
+
this._BRSearchCallbackError(results);
|
264
|
+
}
|
285
265
|
|
286
|
-
/**
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
266
|
+
/**
|
267
|
+
* @private draws search results error
|
268
|
+
* @param {SearchInsideResults} results
|
269
|
+
*/
|
270
|
+
_BRSearchCallbackError(results) {
|
271
|
+
this.searchResults = results;
|
272
|
+
const payload = {
|
273
|
+
term: this.searchTerm,
|
274
|
+
results,
|
275
|
+
instance: this.br,
|
276
|
+
};
|
277
|
+
if (results.error) {
|
278
|
+
this.br.trigger('SearchCallbackError', payload);
|
279
|
+
} else if (0 == results.matches.length) {
|
280
|
+
if (false === results.indexed) {
|
281
|
+
this.br.trigger('SearchCallbackBookNotIndexed', payload);
|
282
|
+
return;
|
283
|
+
}
|
284
|
+
this.br.trigger('SearchCallbackEmpty', payload);
|
285
|
+
}
|
305
286
|
}
|
306
|
-
this.trigger('SearchCallback', { results, options, instance: this });
|
307
|
-
};
|
308
287
|
|
309
|
-
/**
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
*/
|
314
|
-
|
315
|
-
|
316
|
-
};
|
288
|
+
/**
|
289
|
+
* updates search on-page highlights controller
|
290
|
+
*/
|
291
|
+
updateSearchHilites() {
|
292
|
+
/** @type {SearchInsideMatch[]} */
|
293
|
+
const matches = this.searchResults?.matches || [];
|
294
|
+
/** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
|
295
|
+
const boxesByIndex = {};
|
317
296
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
term: this.searchTerm,
|
329
|
-
instance: this,
|
330
|
-
};
|
331
|
-
if (results.error) {
|
332
|
-
const payload = Object.assign({}, basePayload, { results });
|
333
|
-
this.trigger('SearchCallbackError', payload);
|
334
|
-
} else if (0 == results.matches.length) {
|
335
|
-
if (false === results.indexed) {
|
336
|
-
this.trigger('SearchCallbackBookNotIndexed', basePayload);
|
337
|
-
return;
|
297
|
+
// Clear any existing svg layers
|
298
|
+
this.removeSearchHilites();
|
299
|
+
|
300
|
+
// Group by pageIndex
|
301
|
+
for (const match of matches) {
|
302
|
+
for (const box of match.par[0].boxes) {
|
303
|
+
const pageIndex = this.br.book.leafNumToIndex(box.page);
|
304
|
+
const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
|
305
|
+
pageBoxes.push(box);
|
306
|
+
}
|
338
307
|
}
|
339
|
-
this.trigger('SearchCallbackEmpty', basePayload);
|
340
|
-
}
|
341
|
-
};
|
342
308
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
// Clear any existing svg layers
|
353
|
-
this.removeSearchHilites();
|
354
|
-
|
355
|
-
// Group by pageIndex
|
356
|
-
for (const match of matches) {
|
357
|
-
for (const box of match.par[0].boxes) {
|
358
|
-
const pageIndex = this.book.leafNumToIndex(box.page);
|
359
|
-
const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
|
360
|
-
pageBoxes.push(box);
|
309
|
+
// update any already created pages
|
310
|
+
for (const [pageIndexString, boxes] of Object.entries(boxesByIndex)) {
|
311
|
+
const pageIndex = parseFloat(pageIndexString);
|
312
|
+
const page = this.br.book.getPage(pageIndex);
|
313
|
+
const pageContainers = this.br.getActivePageContainerElementsForIndex(pageIndex);
|
314
|
+
for (const container of pageContainers) {
|
315
|
+
renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
|
316
|
+
}
|
361
317
|
}
|
318
|
+
|
319
|
+
this._searchBoxesByIndex = boxesByIndex;
|
362
320
|
}
|
363
321
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
for (const container of pageContainers) {
|
370
|
-
renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
|
371
|
-
}
|
322
|
+
/**
|
323
|
+
* remove search highlights
|
324
|
+
*/
|
325
|
+
removeSearchHilites() {
|
326
|
+
$(this.br.getActivePageContainerElements()).find('.searchHiliteLayer').remove();
|
372
327
|
}
|
373
328
|
|
374
|
-
|
375
|
-
|
329
|
+
/**
|
330
|
+
* Goes to the page specified. If the page is not viewable, tries to load the page
|
331
|
+
* FIXME Most of this logic is IA specific, and should be less integrated into here
|
332
|
+
* or at least more configurable.
|
333
|
+
* @param {number} matchIndex
|
334
|
+
*/
|
335
|
+
async jumpToMatch(matchIndex) {
|
336
|
+
const match = this.searchResults?.matches[matchIndex];
|
337
|
+
const book = this.br.book;
|
338
|
+
const pageIndex = book.leafNumToIndex(match.par[0].page);
|
339
|
+
const page = book.getPage(pageIndex);
|
340
|
+
const onNearbyPage = Math.abs(this.br.currentIndex() - pageIndex) < 3;
|
341
|
+
let makeUnviewableAtEnd = false;
|
342
|
+
if (!page.isViewable) {
|
343
|
+
const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
|
344
|
+
id: this.options.bookId,
|
345
|
+
subprefix: this.options.subPrefix,
|
346
|
+
leafNum: page.leafNum,
|
347
|
+
})).then(r => r.json());
|
348
|
+
|
349
|
+
for (const leafNum of resp.value) {
|
350
|
+
book.getPage(book.leafNumToIndex(leafNum)).makeViewable();
|
351
|
+
}
|
376
352
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
}
|
353
|
+
// not able to show page; make the page viewable anyways so that it can
|
354
|
+
// actually open. On IA, it has a fallback to a special error page.
|
355
|
+
if (!resp.value.length) {
|
356
|
+
book.getPage(pageIndex).makeViewable();
|
357
|
+
makeUnviewableAtEnd = true;
|
358
|
+
}
|
383
359
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
* @param {number} matchIndex
|
390
|
-
*/
|
391
|
-
BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
|
392
|
-
const match = this.searchResults?.matches[matchIndex];
|
393
|
-
const book = this.book;
|
394
|
-
const pageIndex = book.leafNumToIndex(match.par[0].page);
|
395
|
-
const page = book.getPage(pageIndex);
|
396
|
-
const onNearbyPage = Math.abs(this.currentIndex() - pageIndex) < 3;
|
397
|
-
let makeUnviewableAtEnd = false;
|
398
|
-
if (!page.isViewable) {
|
399
|
-
const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
|
400
|
-
id: this.options.bookId,
|
401
|
-
subprefix: this.options.subPrefix,
|
402
|
-
leafNum: page.leafNum,
|
403
|
-
})).then(r => r.json());
|
404
|
-
|
405
|
-
for (const leafNum of resp.value) {
|
406
|
-
book.getPage(book.leafNumToIndex(leafNum)).makeViewable();
|
360
|
+
// Trigger an update of book
|
361
|
+
this.br._modes.mode1Up.mode1UpLit.updatePages();
|
362
|
+
if (this.br.activeMode == this.br._modes.mode1Up) {
|
363
|
+
await this.br._modes.mode1Up.mode1UpLit.updateComplete;
|
364
|
+
}
|
407
365
|
}
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
book.getPage(pageIndex).makeViewable();
|
413
|
-
makeUnviewableAtEnd = true;
|
366
|
+
/* this updates the URL */
|
367
|
+
if (!this.br._isIndexDisplayed(pageIndex)) {
|
368
|
+
this.suppressFragmentChange = false;
|
369
|
+
this.br.jumpToIndex(pageIndex);
|
414
370
|
}
|
415
371
|
|
416
|
-
//
|
417
|
-
|
418
|
-
|
419
|
-
await this._modes.mode1Up.mode1UpLit.updateComplete;
|
372
|
+
// Reset it to unviewable if it wasn't resolved
|
373
|
+
if (makeUnviewableAtEnd) {
|
374
|
+
book.getPage(pageIndex).makeViewable(false);
|
420
375
|
}
|
421
|
-
}
|
422
|
-
/* this updates the URL */
|
423
|
-
if (!this._isIndexDisplayed(pageIndex)) {
|
424
|
-
this.suppressFragmentChange = false;
|
425
|
-
this.jumpToIndex(pageIndex);
|
426
|
-
}
|
427
376
|
|
428
|
-
|
429
|
-
|
430
|
-
|
377
|
+
// Scroll/flash in the ui
|
378
|
+
const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
|
379
|
+
if ($boxes.length) {
|
380
|
+
$boxes.css('animation', 'none');
|
381
|
+
$boxes[0].scrollIntoView({
|
382
|
+
// Only vertically center the highlight if we're in 1up or in full screen. In
|
383
|
+
// 2up, if we're not fullscreen, the whole body gets scrolled around to try to
|
384
|
+
// center the highlight 🙄 See:
|
385
|
+
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
|
386
|
+
// Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
|
387
|
+
// full-width, and covers up the last line of the highlight.
|
388
|
+
block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
|
389
|
+
inline: 'center',
|
390
|
+
behavior: onNearbyPage ? 'smooth' : 'auto',
|
391
|
+
});
|
392
|
+
// wait for animation to start
|
393
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
394
|
+
$boxes.removeAttr("style");
|
395
|
+
}
|
431
396
|
}
|
432
397
|
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
// full-width, and covers up the last line of the highlight.
|
444
|
-
block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
|
445
|
-
inline: 'center',
|
446
|
-
behavior: onNearbyPage ? 'smooth' : 'auto',
|
447
|
-
});
|
448
|
-
// wait for animation to start
|
449
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
450
|
-
$boxes.removeAttr("style");
|
398
|
+
/**
|
399
|
+
* Removes all search pins
|
400
|
+
*/
|
401
|
+
removeSearchResults(suppressFragmentChange = false) {
|
402
|
+
this.removeSearchHilites(); //be sure to set all box.divs to null
|
403
|
+
this.searchTerm = null;
|
404
|
+
this.searchResults = null;
|
405
|
+
if (!suppressFragmentChange) {
|
406
|
+
this.br.trigger(BookReader.eventNames.fragmentChange);
|
407
|
+
}
|
451
408
|
}
|
452
|
-
}
|
409
|
+
}
|
410
|
+
BookReader?.registerPlugin('search', SearchPlugin);
|
453
411
|
|
454
412
|
/**
|
455
|
-
*
|
413
|
+
* @typedef {object} SearchOptions
|
414
|
+
* @property {boolean} goToFirstResult
|
415
|
+
* @property {boolean} disablePopup
|
416
|
+
* @property {boolean} suppressFragmentChange
|
417
|
+
* @property {(null|function)} error (deprecated)
|
418
|
+
* @property {(null|function)} success (deprecated)
|
456
419
|
*/
|
457
|
-
BookReader.prototype.removeSearchResults = function(suppressFragmentChange = false) {
|
458
|
-
this.removeSearchHilites(); //be sure to set all box.divs to null
|
459
|
-
this.searchTerm = null;
|
460
|
-
this.searchResults = null;
|
461
|
-
if (!suppressFragmentChange) {
|
462
|
-
this.trigger(BookReader.eventNames.fragmentChange);
|
463
|
-
}
|
464
|
-
};
|
465
420
|
|
466
421
|
/**
|
467
|
-
*
|
468
|
-
* @
|
422
|
+
* @typedef {object} SearchInsideMatchBox
|
423
|
+
* @property {number} page
|
424
|
+
* @property {number} r
|
425
|
+
* @property {number} l
|
426
|
+
* @property {number} b
|
427
|
+
* @property {number} t
|
428
|
+
* @property {HTMLDivElement} [div]
|
429
|
+
* @property {number} matchIndex This is a fake field! not part of the API response. The index of the match that contains this box in total search results matches.
|
469
430
|
*/
|
470
|
-
BookReader.prototype.searchHighlightVisible = function() {
|
471
|
-
const results = this.searchResults;
|
472
|
-
let visiblePages = [];
|
473
|
-
if (null == results) return false;
|
474
|
-
|
475
|
-
if (this.constMode2up == this.mode) {
|
476
|
-
visiblePages = [this.twoPage.currentIndexL, this.twoPage.currentIndexR];
|
477
|
-
} else if (this.constMode1up == this.mode) {
|
478
|
-
visiblePages = [this.currentIndex()];
|
479
|
-
} else {
|
480
|
-
return false;
|
481
|
-
}
|
482
431
|
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
432
|
+
/**
|
433
|
+
* @typedef {object} SearchInsideMatch
|
434
|
+
* @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
|
435
|
+
* @property {string} displayPageNumber (fake field) The page number as it should be displayed in the UI.
|
436
|
+
* @property {string} html (computed field) The html-escaped raw html to display in the UI.
|
437
|
+
* @property {string} text
|
438
|
+
* @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
|
439
|
+
*/
|
491
440
|
|
492
|
-
|
493
|
-
}
|
441
|
+
/**
|
442
|
+
* @typedef {object} SearchInsideResults
|
443
|
+
* @property {string} q
|
444
|
+
* @property {string} error
|
445
|
+
* @property {SearchInsideMatch[]} matches
|
446
|
+
* @property {boolean} indexed
|
447
|
+
*/
|