Skip to content

Commit ce9b034

Browse files
committed
feat: write a custom search engine and improve the implementation
- set back max results to 500 - add some security in the search process - make progress bars behave better and smoother on big projects (Drupal)
1 parent 0e8c737 commit ce9b034

File tree

2 files changed

+104
-35
lines changed

2 files changed

+104
-35
lines changed

src/Resources/themes/default/doctum.js.twig

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var Doctum = {
2424
doctumSearchAutoCompleteProgressBarPercent: 0,
2525
/** @var autoComplete|null */
2626
autoCompleteJS: null,
27+
querySearchSecurityRegex: /([^a-zA-Z:\\\\_\s])/gi,
2728
buildTreeNode: function (treeNode, htmlNode, treeOpenLevel) {
2829
var ulNode = document.createElement('ul');
2930
for (var childKey in treeNode.c) {
@@ -151,41 +152,65 @@ var Doctum = {
151152
document.getElementById('search-form').submit();
152153
});
153154
Doctum.doctumSearchAutoComplete.addEventListener('navigate', function (event) {
155+
Doctum.markInProgress();
154156
// Set selection in text box
155157
if (typeof event.detail.selection.value === 'object') {
156158
Doctum.doctumSearchAutoComplete.value = event.detail.selection.value.n;
157159
}
158160
});
161+
Doctum.doctumSearchAutoComplete.addEventListener('results', function (event) {
162+
Doctum.markProgressFinished();
163+
});
159164
});
160165
}
161166
// Check if the lib is loaded
162167
if (typeof autoComplete === 'function') {
163168
Doctum.bootAutoComplete();
164169
}
165170
},
166-
loadAutoCompleteData: function (query) {
167-
return new Promise(function (resolve, reject) {
168-
if (Doctum.autoCompleteData !== null) {
169-
resolve(Doctum.autoCompleteData);
170-
return;
171-
}
171+
markInProgress: function () {
172172
Doctum.doctumSearchAutoCompleteProgressBarContainer.className = 'search-bar';
173173
Doctum.doctumSearchAutoCompleteProgressBar.className = 'progress-bar indeterminate';
174174
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
175175
DoctumSearch.doctumSearchPageAutoCompleteProgressBarContainer.className = 'search-bar';
176176
DoctumSearch.doctumSearchPageAutoCompleteProgressBar.className = 'progress-bar indeterminate';
177177
}
178+
},
179+
markProgressFinished: function () {
180+
Doctum.doctumSearchAutoCompleteProgressBarContainer.className = 'search-bar hidden';
181+
Doctum.doctumSearchAutoCompleteProgressBar.className = 'progress-bar';
182+
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
183+
DoctumSearch.doctumSearchPageAutoCompleteProgressBarContainer.className = 'search-bar hidden';
184+
DoctumSearch.doctumSearchPageAutoCompleteProgressBar.className = 'progress-bar';
185+
}
186+
},
187+
makeProgess: function () {
188+
Doctum.makeProgressOnProgressBar(
189+
Doctum.doctumSearchAutoCompleteProgressBarPercent,
190+
Doctum.doctumSearchAutoCompleteProgressBar
191+
);
192+
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
193+
Doctum.makeProgressOnProgressBar(
194+
Doctum.doctumSearchAutoCompleteProgressBarPercent,
195+
DoctumSearch.doctumSearchPageAutoCompleteProgressBar
196+
);
197+
}
198+
},
199+
loadAutoCompleteData: function (query) {
200+
return new Promise(function (resolve, reject) {
201+
if (Doctum.autoCompleteData !== null) {
202+
resolve(Doctum.autoCompleteData);
203+
return;
204+
}
205+
Doctum.markInProgress();
178206
function reqListener() {
179207
Doctum.autoCompleteLoading = false;
180208
Doctum.autoCompleteData = JSON.parse(this.responseText).items;
181-
Doctum.doctumSearchAutoCompleteProgressBarContainer.className = 'search-bar hidden';
182-
Doctum.doctumSearchAutoCompleteProgressBar.className = 'progress-bar';
183-
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
184-
DoctumSearch.doctumSearchPageAutoCompleteProgressBarContainer.className = 'search-bar hidden';
185-
DoctumSearch.doctumSearchPageAutoCompleteProgressBar.className = 'progress-bar';
186-
}
209+
Doctum.markProgressFinished();
187210

188-
resolve(Doctum.autoCompleteData);
211+
setTimeout(function () {
212+
resolve(Doctum.autoCompleteData);
213+
}, 50);// Let the UI render once before sending the results for processing. This gives time to the progress bar to hide
189214
}
190215
function reqError(err) {
191216
Doctum.autoCompleteLoading = false;
@@ -200,26 +225,12 @@ var Doctum = {
200225
oReq.onprogress = function (pe) {
201226
if (pe.lengthComputable) {
202227
Doctum.doctumSearchAutoCompleteProgressBarPercent = parseInt(pe.loaded / pe.total * 100, 10);
203-
Doctum.makeProgressOnProgressBar(
204-
Doctum.doctumSearchAutoCompleteProgressBarPercent,
205-
Doctum.doctumSearchAutoCompleteProgressBar
206-
);
207-
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
208-
Doctum.makeProgressOnProgressBar(
209-
Doctum.doctumSearchAutoCompleteProgressBarPercent,
210-
DoctumSearch.doctumSearchPageAutoCompleteProgressBar
211-
);
212-
}
228+
Doctum.makeProgess();
213229
}
214-
}
230+
};
215231
oReq.onloadend = function (_) {
216-
Doctum.doctumSearchAutoCompleteProgressBarContainer.className = 'search-bar hidden';
217-
Doctum.doctumSearchAutoCompleteProgressBar.className = 'progress-bar';
218-
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
219-
DoctumSearch.doctumSearchPageAutoCompleteProgressBarContainer.className = 'search-bar hidden';
220-
DoctumSearch.doctumSearchPageAutoCompleteProgressBar.className = 'progress-bar';
221-
}
222-
}
232+
Doctum.markProgressFinished();
233+
};
223234
oReq.open('get', Doctum.autoCompleteDataUrl, true);
224235
oReq.send();
225236
});
@@ -237,27 +248,78 @@ var Doctum = {
237248
progressBar.setAttribute(
238249
'aria-valuenow', percentage
239250
);
251+
},
252+
searchEngine: function (query, record) {
253+
if (typeof query !== 'string') {
254+
return '';
255+
}
256+
// replace all (mode = g) spaces and non breaking spaces (\s) by pipes
257+
// g = global mode to mark also the second word searched
258+
// i = case insensitive
259+
// how this function works:
260+
// First: search if the query has the keywords in sequence
261+
// Second: replace the keywords by a mark and leave all the text in between non marked
262+
{% endverbatim -%}
263+
{#
264+
Case 1: search for "net sample"
265+
Data: net_sample
266+
Result <mark>net</mark>_<mark>sample</mark>
267+
Case 1: search for "n t sa"
268+
Data: net_sample, ample, glamples, notDateSa
269+
Result <mark>n</mark>e<mark>t</mark>_<mark>sa</mark>mple, <mark>n</mark>o<mark>t</mark>Da<mark>t</mark>e<mark>Sa</mark>
270+
#}
271+
{% verbatim %}
272+
if (record.match(new RegExp('(' + query.replace(/\s/g, ').*(') + ')', 'gi')) === null) {
273+
return '';// Does not match
274+
}
240275

276+
var replacedRecord = record.replace(new RegExp('(' + query.replace(/\s/g, '|') + ')', 'gi'), function (group) {
277+
return '<mark class="auto-complete-highlight">' + group + '</mark>';
278+
});
279+
280+
if (replacedRecord !== record) {
281+
return replacedRecord;// This should not happen but just in case there was no match done
282+
}
283+
284+
return '';
285+
},
286+
/**
287+
* Clean the search query
288+
*
289+
* @param string query
290+
* @return string
291+
*/
292+
cleanSearchQuery: function (query) {
293+
// replace any chars that could lead to injecting code in our regex
294+
// remove start or end spaces
295+
// replace backslashes by an escaped version, use case in search: \myRootFunction
296+
return query.replace(Doctum.querySearchSecurityRegex, '').trim().replace(/\\/g, '\\\\');
241297
},
242298
bootAutoComplete: function () {
243299
Doctum.autoCompleteJS = new autoComplete(
244300
{
245301
selector: '#doctum-search-auto-complete',
246-
searchEngine: 'loose',
302+
searchEngine: function (query, record) {
303+
return Doctum.searchEngine(query, record);
304+
},
247305
submit: true,
248306
data: {
249307
src: function (q) {
308+
Doctum.markInProgress();
250309
return Doctum.loadAutoCompleteData(q);
251310
},
252311
keys: ['n'],// Data 'Object' key to be searched
253312
cache: false, // Is not compatible with async fetch of data
254313
},
314+
query: (input) => {
315+
return Doctum.cleanSearchQuery(input);
316+
},
255317
resultsList: {
256318
tag: 'ul',
257319
class: 'auto-complete-dropdown-menu',
258320
destination: '#auto-complete-results',
259321
position: 'afterbegin',
260-
maxResults: 99999,
322+
maxResults: 500,
261323
noResults: false,
262324
},
263325
resultItem: {

src/Resources/themes/default/search.twig

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@
128128
DoctumSearch.autoCompleteJS = new autoComplete(
129129
{
130130
selector: '#search',
131-
searchEngine: 'loose',
131+
searchEngine: function (query, record) {
132+
return Doctum.searchEngine(query, record);
133+
},
132134
submit: true,
133135
data: {
134136
src: function (q) {
@@ -137,12 +139,15 @@
137139
keys: ['n'],// Data 'Object' key to be searched
138140
cache: false, // Is not compatible with async fetch of data
139141
},
142+
query: (input) => {
143+
return Doctum.cleanSearchQuery(input);
144+
},
140145
resultsList: {
141146
tag: 'ul',
142147
class: 'search-results',
143148
destination: '#search-results-container',
144149
position: 'afterbegin',
145-
maxResults: 99999,
150+
maxResults: 500,
146151
noResults: false,
147152
},
148153
resultItem: {
@@ -199,9 +204,11 @@
199204
},
200205
}
201206
);
207+
Doctum.markInProgress();
202208
DoctumSearch.autoCompleteJS.start(DoctumSearch.searchTerm);
203209
DoctumSearch.autoCompleteJS.unInit();// Stop the work, wait for the user to hit submit
204210
document.getElementById('search').addEventListener('results', function (event) {
211+
Doctum.markProgressFinished();
205212
if (event.detail.results.length === 0) {
206213
DoctumSearch.showNoResults();
207214
}

0 commit comments

Comments
 (0)