View | Details | Raw Unified | Return to bug 27113
Collapse All | Expand All

(-)a/admin/searchengine/elasticsearch/field_config.yaml (+4 lines)
Lines 47-52 search: Link Here
47
        type: text
47
        type: text
48
        analyzer: analyzer_phrase
48
        analyzer: analyzer_phrase
49
        search_analyzer: analyzer_phrase
49
        search_analyzer: analyzer_phrase
50
      autocomplete:
51
        type: text
52
        analyzer: autocomplete
53
        search_analyzer: standard
50
      raw:
54
      raw:
51
        type: keyword
55
        type: keyword
52
        normalizer: nfkc_cf_normalizer
56
        normalizer: nfkc_cf_normalizer
(-)a/admin/searchengine/elasticsearch/index_config.yaml (+14 lines)
Lines 2-7 Link Here
2
# Index configuration that defines how different analyzers work.
2
# Index configuration that defines how different analyzers work.
3
index:
3
index:
4
  analysis:
4
  analysis:
5
    tokenizer:
6
      autocomplete_tokenizer:
7
        type: edge_ngram
8
        min_gram: 1
9
        max_gram: 10
10
        token_chars:
11
          - letter
12
          - digit
5
    analyzer:
13
    analyzer:
6
      # Phrase analyzer is used for phrases (exact phrase match)
14
      # Phrase analyzer is used for phrases (exact phrase match)
7
      analyzer_phrase:
15
      analyzer_phrase:
Lines 10-15 index: Link Here
10
          - icu_folding
18
          - icu_folding
11
        char_filter:
19
        char_filter:
12
          - punctuation
20
          - punctuation
21
      autocomplete:
22
        type: custom
23
        filter:
24
          - icu_folding
25
          - lowercase
26
        tokenizer: autocomplete_tokenizer
13
      analyzer_standard:
27
      analyzer_standard:
14
        tokenizer: icu_tokenizer
28
        tokenizer: icu_tokenizer
15
        filter:
29
        filter:
(-)a/api/elasticsearch/intranet-autocomplete.pl (+105 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use strict;
4
use warnings;
5
use CGI qw ( -utf8 );
6
use v5.10;
7
use JSON;
8
use Koha::SearchEngine::Search;
9
use Switch;
10
use utf8;
11
use Text::Unaccent;
12
use CGI::Session;
13
14
my $searcher = Koha::SearchEngine::Search->new({index => 'biblios'});
15
my $cgi = CGI->new;
16
my $session = CGI::Session->new();
17
18
$session->param(-name=>'analyzer', -value=>"autocomplete");
19
$session->param(-name=>'prefix', -value=>$cgi->param("prefix"));
20
$session->param(-name=>'q', -value=>$cgi->param("q"));
21
$session->param(-name=>'key', -value=>$cgi->param("key"));
22
$session->param(-name=>'token_counter', -value=>$cgi->param("token_counter"));
23
$session->expire('+1h');
24
25
#Chose GET in key parametre
26
given ($session->param("key")) {
27
  when ("autocomplete") {
28
    my @prefix = split /,/, $session->param("prefix");
29
    # how fields for autocomplete
30
    my $length = scalar @prefix;
31
    #search by many prefix fields
32
    if ($length > 1){
33
      print $cgi->header("application/json");
34
      print to_json(GetAutocompleteAllIdx($session->param("q"), \@prefix, $session->param("analyzer"), $session->param("token_counter"), $searcher));
35
    }
36
    #search by one prefix field
37
    elsif ($length == 1) {
38
      print $cgi->header("application/json");
39
      print to_json(GetAutocompleteOneIdx($session->param("q"), $prefix[0], $session->param("analyzer"), $session->param("token_counter"), $searcher));
40
    }
41
    #no prefix 404
42
    else {
43
      response404JSON();
44
    }
45
  }
46
  #no key 404
47
  default {
48
    response404JSON();
49
  }
50
}
51
52
sub response404JSON {
53
  my $json = JSON->new->utf8;
54
  my $header_type = "application/json";
55
  my $header_status = "404";
56
  my $output = $json->encode({
57
    "error" => "No data",
58
    "description" => "Bad request",
59
  });
60
  print $cgi->header(
61
    -type => $header_type,
62
    -charset => "utf-8",
63
    -status => $header_status
64
  );
65
  print $output;
66
  print "\n";
67
}
68
69
sub GetAutocompleteOneIdx {
70
  my ($cgi_q, $prefix, $analyzer, $token_counter, $searcher) = @_;
71
  my (%query, $results, @source);
72
  #prefix + analyzer
73
  my $prefix_analyzer = $prefix . '.' . $analyzer;
74
  # we can change this variables
75
  my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, ["<strong>"], ["</strong>"]);
76
  push(@source, $prefix);
77
  $query{'_source'} = \@source;
78
  $query{'query'}{'match'}{$prefix_analyzer}{'query'} = unac_string("UTF-8", $cgi_q);
79
  $query{'query'}{'match'}{$prefix_analyzer}{'operator'} = 'and';
80
  #hightlight
81
  $query{'highlight'}{'number_of_fragments'} = $nb_fragments;
82
  $query{'highlight'}{'fragment_size'} = $size_fragment;
83
  $query{'highlight'}{'pre_tags'} = $pre_tags;
84
  $query{'highlight'}{'post_tags'} = $post_tags;
85
  $query{'highlight'}{'fields'}{$prefix_analyzer} = {};
86
  $results = $searcher->search(\%query);
87
  $results->{'val'} = $cgi_q;
88
  $results->{'prefix'} = $prefix;
89
  $results->{'token_counter'} = $token_counter;
90
  return $results;
91
}
92
93
sub GetAutocompleteAllIdx {
94
  my ($cgi_q, $prefix, $analyzer, $token_counter, $searcher) = @_;
95
  my %results;
96
  my $idx = 0;
97
  foreach my $pref ( @$prefix ) {
98
      $results{$idx} = GetAutocompleteOneIdx($cgi_q, $pref, $analyzer, $token_counter, $searcher);
99
      $idx++;
100
  }
101
  $results{'val'} = $cgi_q;
102
  $results{'prefix'} = join( ',', @$prefix );
103
  $results{'token_counter'} = $token_counter;
104
  return \%results;
105
}
(-)a/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl (+8 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    # you can use $dbh here like:
4
    $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('AutocompleteElasticSearch', '0', NULL, NULL, 'YesNo')});
5
6
    # Always end with this (adjust the bug info)
7
    NewVersion( $DBversion, 27113, "Autocomplete input on main page with elasticsearch");
8
}
(-)a/installer/data/mysql/mandatory/sysprefs.sql (-8 / +9 lines)
Lines 53-63 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
53
('AmazonCoverImages','0','','Display Cover Images in staff interface from Amazon Web Services','YesNo'),
53
('AmazonCoverImages','0','','Display Cover Images in staff interface from Amazon Web Services','YesNo'),
54
('AmazonLocale','US','US|CA|DE|FR|IN|JP|UK','Use to set the Locale of your Amazon.com Web Services','Choice'),
54
('AmazonLocale','US','US|CA|DE|FR|IN|JP|UK','Use to set the Locale of your Amazon.com Web Services','Choice'),
55
('AnonSuggestions','0',NULL,'Set to enable Anonymous suggestions to AnonymousPatron borrowernumber','YesNo'),
55
('AnonSuggestions','0',NULL,'Set to enable Anonymous suggestions to AnonymousPatron borrowernumber','YesNo'),
56
('AnonymousPatron','0',NULL,'Set the identifier (borrowernumber) of the anonymous patron. Used for suggestion and checkout history privacy',''),
56
('AnonymousPatron','0',NULL,'Set the identifier (borrowernumber) of the anonymous patron. Used for Suggestion and reading history privacy',''),
57
('ArticleRequests', '0', NULL, 'Enables the article request feature', 'YesNo'),
57
('ArticleRequests', '0', NULL, 'Enables the article request feature', 'YesNo'),
58
('ArticleRequestsLinkControl', 'calc', 'always|calc', 'Control display of article request link on search results', 'Choice'),
58
('ArticleRequestsLinkControl', 'calc', 'always|calc', 'Control display of article request link on search results', 'Choice'),
59
('ArticleRequestsMandatoryFields', '', NULL, 'Comma delimited list of required fields for bibs where article requests rule = ''yes''', 'multiple'),
59
('ArticleRequestsMandatoryFields', '', NULL, 'Comma delimited list of required fields for bibs where article requests rule = ''yes''', 'multiple'),
60
('ArticleRequestsMandatoryFieldsItemOnly', '', NULL, 'Comma delimited list of required fields for bibs where article requests rule = ''item_only''', 'multiple'),
60
('ArticleRequestsMandatoryFieldsItemsOnly', '', NULL, 'Comma delimited list of required fields for bibs where article requests rule = ''item_only''', 'multiple'),
61
('ArticleRequestsMandatoryFieldsRecordOnly', '', NULL, 'Comma delimited list of required fields for bibs where article requests rule = ''bib_only''', 'multiple'),
61
('ArticleRequestsMandatoryFieldsRecordOnly', '', NULL, 'Comma delimited list of required fields for bibs where article requests rule = ''bib_only''', 'multiple'),
62
('AudioAlerts','0','','Enable circulation sounds during checkin and checkout in the staff interface.  Not supported by all web browsers yet.','YesNo'),
62
('AudioAlerts','0','','Enable circulation sounds during checkin and checkout in the staff interface.  Not supported by all web browsers yet.','YesNo'),
63
('AuthDisplayHierarchy','0','','Display authority hierarchies','YesNo'),
63
('AuthDisplayHierarchy','0','','Display authority hierarchies','YesNo'),
Lines 271-277 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
271
('IntranetmainUserblock','','70|10','Add a block of HTML that will display on the intranet home page','Textarea'),
271
('IntranetmainUserblock','','70|10','Add a block of HTML that will display on the intranet home page','Textarea'),
272
('IntranetNav','','70|10','Use HTML tabs to add navigational links to the top-hand navigational bar in the staff interface','Textarea'),
272
('IntranetNav','','70|10','Use HTML tabs to add navigational links to the top-hand navigational bar in the staff interface','Textarea'),
273
('IntranetNumbersPreferPhrase','0',NULL,'Control the use of phr operator in callnumber and standard number staff interface searches','YesNo'),
273
('IntranetNumbersPreferPhrase','0',NULL,'Control the use of phr operator in callnumber and standard number staff interface searches','YesNo'),
274
('intranetreadinghistory','1','','If ON, Checkout history is enabled for all patrons','YesNo'),
274
('intranetreadinghistory','1','','If ON, Reading History is enabled for all patrons','YesNo'),
275
('IntranetReportsHomeHTML', '', NULL, 'Show the following HTML in a div on the bottom of the reports home page', 'Free'),
275
('IntranetReportsHomeHTML', '', NULL, 'Show the following HTML in a div on the bottom of the reports home page', 'Free'),
276
('IntranetSlipPrinterJS','','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','Free'),
276
('IntranetSlipPrinterJS','','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','Free'),
277
('intranetstylesheet','','50','Enter a complete URL to use an alternate layout stylesheet in Intranet','free'),
277
('intranetstylesheet','','50','Enter a complete URL to use an alternate layout stylesheet in Intranet','free'),
Lines 285-291 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
285
('item-level_itypes','1','','If ON, enables Item-level Itemtype / Issuing Rules','YesNo'),
285
('item-level_itypes','1','','If ON, enables Item-level Itemtype / Issuing Rules','YesNo'),
286
('itemBarcodeFallbackSearch','',NULL,'If set, uses scanned item barcodes as a catalogue search if not found as barcodes','YesNo'),
286
('itemBarcodeFallbackSearch','',NULL,'If set, uses scanned item barcodes as a catalogue search if not found as barcodes','YesNo'),
287
('itemBarcodeInputFilter','','whitespace|T-prefix|cuecat|libsuite8|EAN13','If set, allows specification of a item barcode input filter','Choice'),
287
('itemBarcodeInputFilter','','whitespace|T-prefix|cuecat|libsuite8|EAN13','If set, allows specification of a item barcode input filter','Choice'),
288
('itemcallnumber','',NULL,'The MARC field/subfield that is used to calculate the itemcallnumber (Dewey would be 082ab or 092ab; LOC would be 050ab or 090ab) could be 852hi from an item record','free'),
288
('itemcallnumber','082ab',NULL,'The MARC field/subfield that is used to calculate the itemcallnumber (Dewey would be 082ab or 092ab; LOC would be 050ab or 090ab) could be 852hi from an item record','free'),
289
('ItemsDeniedRenewal','','','This syspref allows to define custom rules for denying renewal of specific items.','Textarea'),
289
('ItemsDeniedRenewal','','','This syspref allows to define custom rules for denying renewal of specific items.','Textarea'),
290
('KohaAdminEmailAddress','root@localhost','','Define the email address where patron modification requests are sent','free'),
290
('KohaAdminEmailAddress','root@localhost','','Define the email address where patron modification requests are sent','free'),
291
('KohaManualBaseURL','https://koha-community.org/manual/','','Where is the Koha manual/documentation located?','Free'),
291
('KohaManualBaseURL','https://koha-community.org/manual/','','Where is the Koha manual/documentation located?','Free'),
Lines 423-429 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
423
('OpacMaxItemsToDisplay','50','','Max items to display at the OPAC on a biblio detail','Integer'),
423
('OpacMaxItemsToDisplay','50','','Max items to display at the OPAC on a biblio detail','Integer'),
424
('OpacMetaDescription','','','This description will show in search engine results (160 characters).','Textarea'),
424
('OpacMetaDescription','','','This description will show in search engine results (160 characters).','Textarea'),
425
('OpacMoreSearches', '', NULL, 'Add additional elements to the OPAC more searches bar', 'Textarea'),
425
('OpacMoreSearches', '', NULL, 'Add additional elements to the OPAC more searches bar', 'Textarea'),
426
('OPACMySummaryHTML','','70|10','Enter the HTML that will appear in a column on the \'my summary\' and \'my checkout history\' tabs when a user is logged in to the OPAC. Enter {BIBLIONUMBER}, {TITLE}, {AUTHOR}, or {ISBN} in place of their respective variables in the HTML. Leave blank to disable.','Textarea'),
426
('OPACMySummaryHTML','','70|10','Enter the HTML that will appear in a column on the \'my summary\' and \'my reading history\' tabs when a user is logged in to the OPAC. Enter {BIBLIONUMBER}, {TITLE}, {AUTHOR}, or {ISBN} in place of their respective variables in the HTML. Leave blank to disable.','Textarea'),
427
('OPACMySummaryNote','','','Note to display on the patron summary page. This note only appears if the patron is connected.','Free'),
427
('OPACMySummaryNote','','','Note to display on the patron summary page. This note only appears if the patron is connected.','Free'),
428
('OpacNav','Important links here.','70|10','Use HTML tags to add navigational links to the left-hand navigational bar in OPAC','Textarea'),
428
('OpacNav','Important links here.','70|10','Use HTML tags to add navigational links to the left-hand navigational bar in OPAC','Textarea'),
429
('OpacNavBottom','Important links here.','70|10','Use HTML tags to add navigational links to the left-hand navigational bar in OPAC','Textarea'),
429
('OpacNavBottom','Important links here.','70|10','Use HTML tags to add navigational links to the left-hand navigational bar in OPAC','Textarea'),
Lines 436-445 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
436
('OpacPasswordChange','1',NULL,'If ON, enables patron-initiated password change in OPAC (disable it when using LDAP auth)','YesNo'),
436
('OpacPasswordChange','1',NULL,'If ON, enables patron-initiated password change in OPAC (disable it when using LDAP auth)','YesNo'),
437
('OPACPatronDetails','1','','If OFF the patron details tab in the OPAC is disabled.','YesNo'),
437
('OPACPatronDetails','1','','If OFF the patron details tab in the OPAC is disabled.','YesNo'),
438
('OPACpatronimages','0',NULL,'Enable patron images in the OPAC','YesNo'),
438
('OPACpatronimages','0',NULL,'Enable patron images in the OPAC','YesNo'),
439
('OpacPrivacy','0',NULL,'if ON, allows patrons to define their privacy rules (checkout history)','YesNo'),
439
('OpacPrivacy','0',NULL,'if ON, allows patrons to define their privacy rules (reading history)','YesNo'),
440
('OpacPublic','1',NULL,'Turn on/off public OPAC','YesNo'),
440
('OpacPublic','1',NULL,'Turn on/off public OPAC','YesNo'),
441
('opacreadinghistory','1','','If ON, enables display of Patron Circulation History in OPAC','YesNo'),
441
('opacreadinghistory','1','','If ON, enables display of Patron Circulation History in OPAC','YesNo'),
442
('OpacRenewalAllowed','1',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'),
442
('OpacRenewalAllowed','0',NULL,'If ON, users can renew their issues directly from their OPAC account','YesNo'),
443
('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|none','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'),
443
('OpacRenewalBranch','checkoutbranch','itemhomebranch|patronhomebranch|checkoutbranch|none','Choose how the branch for an OPAC renewal is recorded in statistics','Choice'),
444
('OPACReportProblem', 0, NULL, 'Allow patrons to submit problem reports for OPAC pages to the library or Koha Administrator', 'YesNo'),
444
('OPACReportProblem', 0, NULL, 'Allow patrons to submit problem reports for OPAC pages to the library or Koha Administrator', 'YesNo'),
445
('OpacResetPassword','0','','Shows the ''Forgot your password?'' link in the OPAC','YesNo'),
445
('OpacResetPassword','0','','Shows the ''Forgot your password?'' link in the OPAC','YesNo'),
Lines 722-726 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
722
('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'),
722
('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'),
723
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
723
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
724
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
724
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
725
('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo')
725
('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'),
726
('AutocompleteElasticSearch','0',NULL,NULL,'YesNo')
726
;
727
;
(-)a/koha-tmpl/intranet-tmpl/js/intranet-elasticsearch/intranet-autocomplete.js (+270 lines)
Line 0 Link Here
1
/* OPAC JS file AutocompleteElasticSearch */
2
/* prefix for search ES */
3
var es_prefix = {
4
    'au': 'author',
5
    'pb': 'publisher',
6
    'se': 'title-series',
7
    'su': 'subject',
8
    'ti': 'title-cover',
9
    /* for all */
10
    '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'],
11
    'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher']
12
};
13
14
/* stop class for elements name=["q"] */
15
var stop_class_input = ["form-field-value"];
16
17
var url_request = '/cgi-bin/koha/api/elasticsearch/intranet-autocomplete.pl?q=';
18
/* query for elasticsearch encode*/
19
var query_url_encode = {
20
    '\'':'%5C%27', /* \\' for decode */
21
    '+': ''
22
};
23
/* query for elasticsearch decode*/
24
var query_url_decode = {
25
    '\'':"\\'",
26
    '+': ''
27
};
28
/* count of lines for autocomplete */
29
var nb_autocomplete = 10;
30
/* key API */
31
var key = 'autocomplete';
32
33
function AutocompleteInitIntranet(){
34
    /* vars for class position absolute autocomplete */
35
    var left = "0px";
36
    var right = "0px";
37
    var top = "";
38
    /* get all input name q for search */
39
    var input_q = document.getElementsByName("q");
40
    for (var nb = 0; nb < input_q.length; nb++){
41
        /* addEventListener for every 'input' */
42
        if (!stop_class_input.includes(input_q[nb].className)){
43
            autocomplete(input_q[nb], nb, left, right, top);
44
        }
45
    };
46
};
47
48
function autocomplete(inp, nb, left, right) {
49
    var select_idx = document.getElementsByName("idx");
50
    /* autocomplete off for input */
51
    inp.setAttribute("autocomplete", "off");
52
    /* get parent of input */
53
    var parent_inp = $(inp).parent();
54
    /* get element after input */
55
    var next_elem_inp = inp.nextElementSibling;
56
    /* create new div with position relative for class .autocomplete with absolute */
57
    var div_relative = document.createElement('div');
58
    $(div_relative).addClass( "autocomplete" );
59
    div_relative.append(inp);
60
    /* input doesn't have an elem after, add it to parent */
61
    if (next_elem_inp === null){
62
        parent_inp.append( div_relative );
63
    }  else {  // input has an elem after, add elem after it
64
        next_elem_inp.before(div_relative);
65
    };
66
    var currentFocus;
67
    /*execute a function when someone writes in the text field:*/
68
    var token_counter = 0;
69
    inp.addEventListener("input", function(e) {
70
        var a, val = this.value;
71
        /* var for async compare */
72
        var tmp_input = this.value.replace(/[+']/g, function(matched){
73
            return query_url_decode[matched];
74
        });
75
        token_counter++;
76
        currentFocus = -1;
77
        if (document.getElementsByClassName("autocomplete-items").length !== 0){
78
            a = document.getElementsByClassName("autocomplete-items")[0];
79
        } else {
80
            /*create a DIV element that will contain the items (values):*/
81
            a = document.createElement("DIV");
82
            a.setAttribute("id", this.id + "autocomplete-list");
83
            a.setAttribute("class", "autocomplete-items");
84
            /*append the DIV element as a child of the autocomplete container:*/
85
            this.parentNode.appendChild(a);
86
            /*append position absolute left/right:*/
87
            $(".autocomplete-items").css("left",left);
88
            $(".autocomplete-items").css("right",right);
89
        };
90
        /* get es_prefix key for builder */
91
        var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb);
92
        chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1');
93
        if (chose_prefix !== null){
94
            var prefix = es_prefix[chose_prefix].toString();
95
            val = val.replace(/[+']/g, function(matched){
96
                return query_url_encode[matched];
97
            });
98
            if (tmp_input == '' || tmp_input == null){
99
                closeAllLists();
100
                token_counter = 0;
101
            } else {
102
                $.ajax({
103
                    type: 'GET',
104
                    url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter,
105
                    success: function (data) {
106
                        //console.log(data);
107
                        if (data.length !== 0){
108
                            var myset; //Set for Autocomplete unique
109
                            /* autocomplete for all prefix */
110
                            if (chose_prefix === 'kw' || chose_prefix === ''){
111
                                myset = GetSetAutocompleteAllIdx(data, prefix, key);
112
                            } else { // autocomplete for one prefix
113
                                myset = GetSetAutocompleteOneIdx(data, prefix, key);
114
                            };
115
                            /* append set to autocomplete */
116
                            if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){
117
                                a.innerHTML = "";
118
                                for (let item of myset){
119
                                    a.appendChild(CreateDivItemAutocomplete(item, val));
120
                                };
121
                            };
122
                        } else {
123
                            closeAllLists(this);
124
                        };
125
                    },
126
                    error: function (data) {
127
                        console.log(data);
128
                    },
129
                });
130
            }
131
132
        };
133
    });
134
    /* get value for tag with name idx */
135
    function GetValueIdx(elem, nb){
136
        switch (elem[0].tagName){
137
            case 'INPUT':
138
                return elem[0].value;
139
            case 'SELECT':
140
                return select_idx[nb].options[select_idx[nb].selectedIndex].value;
141
            default:
142
                return null;
143
        };
144
    };
145
    /* get autocomplete for only one prefix title/author/etc... */
146
    function GetSetAutocompleteOneIdx(data, prefix, key){
147
        let myset = new Set();
148
        let tmp_data = data['hits']['hits'];
149
        for (let i = 0; i < tmp_data.length; i++) {
150
            for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){
151
                /* div with data for autocomplete */
152
                let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j];
153
                tmp = tmp.replace(/^\[/g, '');
154
                tmp = tmp.replace(/\]+$/g, '');
155
                myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, ''));
156
                if (myset.size >= nb_autocomplete) break;
157
            };
158
            if (myset.size >= nb_autocomplete) break;
159
        };
160
        return myset;
161
    };
162
    /* get autocomplete for all prefix */
163
    function GetSetAutocompleteAllIdx(data, prefix, key){
164
        let myset = new Set();
165
        var pref = prefix.split(",");
166
        for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length
167
            if (data[k] != '' && data[k] != null){
168
                let tmp_data = data[k]['hits']['hits'];
169
                for (i = 0; i < tmp_data.length; i++) {
170
                    for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){
171
                        /* div with data for autocomplete */
172
                        let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j]
173
                        myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, ''));
174
                        if (myset.size >= nb_autocomplete) break;
175
                    };
176
                    if (myset.size >= nb_autocomplete) break;
177
                };
178
                if (myset.size >= nb_autocomplete) break;
179
            }
180
        }
181
        return myset;
182
    };
183
184
    /*execute a function presses a key on the keyboard:*/
185
    inp.addEventListener("keydown", function(e) {
186
        var x = document.getElementById(this.id + "autocomplete-list");
187
        if (x) x = x.getElementsByTagName("div");
188
        if (e.keyCode == 40) { //DOWN
189
            /*If the arrow DOWN key is pressed,
190
            increase the currentFocus variable:*/
191
            currentFocus++;
192
            /*and and make the current item more visible:*/
193
            addActive(x);
194
        } else if (e.keyCode == 38) { //up
195
            /*If the arrow UP key is pressed,
196
            decrease the currentFocus variable:*/
197
            currentFocus--;
198
            /*and and make the current item more visible:*/
199
            addActive(x);
200
            e.preventDefault();
201
        } else if (e.keyCode == 13) {
202
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
203
            //e.preventDefault();
204
            if (currentFocus > -1) {
205
                /*and simulate a click on the "active" item:*/
206
                if (x) x[currentFocus].click();
207
            }
208
        }
209
        /* press Esc clear all autocomplete */
210
        else if (e.keyCode == 27) {
211
            closeAllLists();
212
        }
213
        /* press Esc clear all autocomplete */
214
        else if (e.keyCode == 8) {
215
            closeAllLists();
216
        };
217
    });
218
    function addActive(x) {
219
        /*a function to classify an item as "active":*/
220
        if (!x) return false;
221
        /*start by removing the "active" class on all items:*/
222
        removeActive(x);
223
        if (currentFocus >= x.length) currentFocus = 0;
224
        if (currentFocus < 0) currentFocus = (x.length - 1);
225
        /*add class "autocomplete-active":*/
226
        x[currentFocus].classList.add("autocomplete-active");
227
        inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim();
228
    };
229
    function removeActive(x) {
230
        /*a function to remove the "active" class from all autocomplete items:*/
231
        for (var i = 0; i < x.length; i++) {
232
            x[i].classList.remove("autocomplete-active");
233
        };
234
    };
235
236
    function closeAllLists(elem) {
237
        /*close all autocomplete lists in the document with class autocomplete-items */
238
        var x = document.getElementsByClassName("autocomplete-items");
239
        for (var i = 0; i < x.length; i++) {
240
            x[i].parentNode.removeChild(x[i])
241
        };
242
    };
243
244
    /* div for one item autocomplete */
245
    function CreateDivItemAutocomplete (elem){
246
        var b = document.createElement("DIV");
247
        // add element ";
248
        b.innerHTML += elem;
249
        /*insert a input field that will hold the current array item's value:*/
250
        b.innerHTML += "<input type='hidden' value=''>";
251
        /*execute a function when someone clicks on the item value (DIV element):*/
252
        b.addEventListener("click", function(e) {
253
            /* insert the value for the autocomplete text field: */
254
            inp.value = this.getElementsByTagName("input")[0].value;
255
            /* normalyzer hightlight without tags */
256
            //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim();
257
            inp.value = e.target.innerText;
258
            /* Submit form click mouse in div */
259
            this.closest("form").submit();
260
        });
261
        return b;
262
    };
263
264
    /*execute a function when someone clicks in the document:*/
265
    document.addEventListener("click", function (e) {
266
        closeAllLists(e.target);
267
    });
268
};
269
270
AutocompleteInitIntranet();
(-)a/koha-tmpl/intranet-tmpl/prog/css/intranet-elasticsearch/intranet-autocomplete.css (+28 lines)
Line 0 Link Here
1
.autocomplete {
2
  /*the container must be positioned relative:*/
3
  position: relative;
4
  display: inline-block;
5
}
6
.autocomplete-items {
7
    position: absolute;
8
    border: 1px solid #d4d4d4;
9
    border-bottom: none;
10
    border-top: none;
11
    z-index: 99;
12
    /*position the autocomplete items to be the same width as the container:*/
13
  }
14
  .autocomplete-items div {
15
    padding: 10px;
16
    cursor: pointer;
17
    background-color: #fff;
18
    border-bottom: 1px solid #d4d4d4;
19
  }
20
  .autocomplete-items div:hover {
21
    /*when hovering an item:*/
22
    background-color: #e9e9e9;
23
  }
24
  .autocomplete-active {
25
    /*when navigating through the items using the arrow keys:*/
26
    background-color: #cedfb1 !important;
27
    color: #ffffff;
28
  }
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc (+5 lines)
Lines 21-26 Link Here
21
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
21
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
22
[% INCLUDE intranetstylesheet.inc %]
22
[% INCLUDE intranetstylesheet.inc %]
23
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
23
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
24
<!-- Intranet inc CSS AutocompleteElasticSearch -->
25
[% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
26
    [% SET Optylesheet = 'intranet-elasticsearch/intranet-autocomplete.css' %]
27
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
28
[% END %]
24
29
25
<script>
30
<script>
26
var Koha = {};
31
var Koha = {};
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc (+4 lines)
Lines 144-147 Link Here
144
    });
144
    });
145
    </script>
145
    </script>
146
[% END %]
146
[% END %]
147
<!-- Intranet inc JS AutocompleteElasticSearch -->
148
[% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
149
	[% Asset.js("js/intranet-elasticsearch/intranet-autocomplete.js") %]
150
[% END %]
147
<!-- / js_includes.inc -->
151
<!-- / js_includes.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref (+9 lines)
Lines 283-285 Searching: Link Here
283
            - LIBRIS base URL
283
            - LIBRIS base URL
284
            - pref: LibrisURL
284
            - pref: LibrisURL
285
            - "Please only change this if you are sure it needs changing."
285
            - "Please only change this if you are sure it needs changing."
286
        -
287
            - pref: AutocompleteElasticSearch
288
              type: boolean
289
              default: no
290
              choices:
291
                  yes: Show
292
                  no: "Don't show"
293
            - looking terms based on a provided text by using an ElasticSearch.
294
        -
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt (-5 / +5 lines)
Lines 94-104 Link Here
94
    <fieldset id="searchterms">
94
    <fieldset id="searchterms">
95
    <legend>Search for </legend>
95
    <legend>Search for </legend>
96
    [% FOREACH search_box IN search_boxes_loop %]
96
    [% FOREACH search_box IN search_boxes_loop %]
97
        [% IF ( search_boxes_label ) %]
97
        [% IF ( search_boxes_label ) %]<div style="text-indent: 4.5em;">[% ELSE %]<div>[% END %]
98
        <div class="search-term-row" style="text-indent: 4.5em;">
99
        [% ELSE %]
100
        <div class="search-term-row">
101
        [% END %]
102
			[% IF ( expanded_options ) %]
98
			[% IF ( expanded_options ) %]
103
            [% IF ( search_box.boolean ) %]
99
            [% IF ( search_box.boolean ) %]
104
                <select name="op">
100
                <select name="op">
Lines 325-330 Link Here
325
            var dad  = line.parentNode;
321
            var dad  = line.parentNode;
326
            dad.appendChild(line.cloneNode(true));
322
            dad.appendChild(line.cloneNode(true));
327
            line.removeChild(ButtonPlus);
323
            line.removeChild(ButtonPlus);
324
            /* Intranet JS AutocompleteElasticSearch */
325
            [% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
326
                AutocompleteInitIntranet();
327
            [% END %]
328
        }
328
        }
329
        var Sticky;
329
        var Sticky;
330
        $(document).ready(function() {
330
        $(document).ready(function() {
(-)a/koha-tmpl/intranet-tmpl/prog/js/staff-global.js (+6 lines)
Lines 76-81 $.fn.selectTabByID = function (tabID) { Link Here
76
    $(".keep_text").on("click",function(){
76
    $(".keep_text").on("click",function(){
77
        var field_index = $(this).parent().index();
77
        var field_index = $(this).parent().index();
78
        keep_text( field_index );
78
        keep_text( field_index );
79
        /* AutocompleteElasticSearch Tab */
80
        var tab = this.hash.substr(1, this.hash.length-1);
81
        /*  Koha.Preference('AutocompleteElasticSearch') == Show */
82
        if (typeof AutocompleteInitIntranet !== "undefined" && tab === 'catalog_search' ){
83
            AutocompleteInitIntranet();
84
        }
79
    });
85
    });
80
86
81
    $(".toggle_element").on("click",function(e){
87
    $(".toggle_element").on("click",function(e){
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css (+29 lines)
Line 0 Link Here
1
/* CSS file AutocompleteElasticSearch */
2
.autocomplete {
3
  /*the container must be positioned relative:*/
4
  position: relative;
5
  display: inline-block;
6
  width: 100%;
7
}
8
.autocomplete-items {
9
    position: absolute;
10
    border: 1px solid #d4d4d4;
11
    border-bottom: none;
12
    border-top: none;
13
    z-index: 99;
14
  }
15
  .autocomplete-items div {
16
    padding: 10px;
17
    cursor: pointer;
18
    background-color: #fff;
19
    border-bottom: 1px solid #d4d4d4;
20
  }
21
  .autocomplete-items div:hover {
22
    /*when hovering an item:*/
23
    background-color: #e9e9e9;
24
  }
25
  .autocomplete-active {
26
    /*when navigating through the items using the arrow keys:*/
27
    background-color: #cedfb1 !important;
28
    color: #ffffff;
29
  }
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc (+5 lines)
Lines 23-28 Link Here
23
        [% SET opaclayoutstylesheet = 'opac.css' %]
23
        [% SET opaclayoutstylesheet = 'opac.css' %]
24
    [% END %]
24
    [% END %]
25
[% END %]
25
[% END %]
26
<!-- OPAC inc CSS AutocompleteElasticSearch -->
27
[% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
28
    [% SET Optylesheet = 'opac-elasticsearch/opac-autocomplete.css' %]
29
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
30
[% END %]
26
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
31
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
27
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
32
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
28
[% ELSE %]
33
[% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (+5 lines)
Lines 207-212 $.widget.bridge('uitooltip', $.ui.tooltip); Link Here
207
    [% END %]
207
    [% END %]
208
    [% IF OpenLibraryCovers || OpenLibrarySearch %]
208
    [% IF OpenLibraryCovers || OpenLibrarySearch %]
209
        var NO_OL_JACKET = _("No cover image available");
209
        var NO_OL_JACKET = _("No cover image available");
210
        var OL_PREVIEW = _("Preview");
210
    [% END %]
211
    [% END %]
211
    [% IF (query_desc) %]
212
    [% IF (query_desc) %]
212
        var query_desc = "[% query_desc | html %]";
213
        var query_desc = "[% query_desc | html %]";
Lines 323-328 $(document).ready(function() { Link Here
323
        [% Koha.Preference('OPACUserJS') | $raw %]
324
        [% Koha.Preference('OPACUserJS') | $raw %]
324
    </script>
325
    </script>
325
[% END %]
326
[% END %]
327
<!-- OPAC *.inc JS AutocompleteElasticSearch -->
328
[% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
329
    [% Asset.js("js/opac-elasticsearch/opac-autocomplete.js") %]
330
[% END %]
326
[% IF SCO_login %]
331
[% IF SCO_login %]
327
    [% SET SCOUserJS = Koha.Preference('SCOUserJS') %]
332
    [% SET SCOUserJS = Koha.Preference('SCOUserJS') %]
328
    [% IF ( SCOUserJS ) %]
333
    [% IF ( SCOUserJS ) %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt (+8 lines)
Lines 526-531 $(document).ready(function() { Link Here
526
        var newLine = thisLine.clone();
526
        var newLine = thisLine.clone();
527
        newLine.find('input').val('');
527
        newLine.find('input').val('');
528
        thisLine.after(newLine);
528
        thisLine.after(newLine);
529
        /* OPAC JS AutocompleteElasticSearch */
530
        [% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
531
            AutocompleteInitOpac();
532
        [% END %]
529
    });
533
    });
530
534
531
    $(document).on("click", '.ButtonLess', function(e) {
535
    $(document).on("click", '.ButtonLess', function(e) {
Lines 534-539 $(document).ready(function() { Link Here
534
           $('.ButtonLess').hide();
538
           $('.ButtonLess').hide();
535
        }
539
        }
536
        $(this).parent().parent().remove();
540
        $(this).parent().parent().remove();
541
        /* OPAC JS AutocompleteElasticSearch */
542
        [% IF ( Koha.Preference('AutocompleteElasticSearch') ) %]
543
            AutocompleteInitOpac();
544
        [% END %]
537
    });
545
    });
538
546
539
</script>
547
</script>
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js (+265 lines)
Line 0 Link Here
1
/* OPAC JS file AutocompleteElasticSearch */
2
/* prefix for search ES */
3
var es_prefix = {
4
    'au': 'author',
5
    'pb': 'publisher',
6
    'se': 'title-series',
7
    'su': 'subject',
8
    'ti': 'title-cover',
9
    /* for all */
10
    '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'],
11
    'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher']
12
};
13
14
var url_request = '/cgi-bin/koha/svc/elasticsearch/opac-autocomplete.pl?q=';
15
/* query for elasticsearch encode*/
16
var query_url_encode = {
17
    '\'':'%5C%27', /* \\' for decode */
18
    '+': ''
19
};
20
/* query for elasticsearch decode*/
21
var query_url_decode = {
22
    '\'':"\\'",
23
    '+': ''
24
};
25
/* count of lines for autocomplete */
26
var nb_autocomplete = 10;
27
/* key API */
28
var key = 'autocomplete';
29
30
function AutocompleteInitOpac(){
31
    /* vars for class position absolute autocomplete */
32
    var left = "0px";
33
    var right = "0px";
34
    var top = "";
35
    /* get all input name q for search */
36
    var input_q = document.getElementsByName("q");
37
    for (var nb = 0; nb < input_q.length; nb++){
38
        /* addEventListener for every 'input' */
39
        autocomplete(input_q[nb], nb, left, right, top);
40
    };
41
};
42
43
function autocomplete(inp, nb, left, right) {
44
    var select_idx = document.getElementsByName("idx");
45
    /* autocomplete off for input */
46
    inp.setAttribute("autocomplete", "off");
47
    /* get parent of input */
48
    var parent_inp = $(inp).parent();
49
    /* get element after input */
50
    var next_elem_inp = inp.nextElementSibling;
51
    /* create new div with position relative for class .autocomplete with absolute */
52
    var div_relative = document.createElement('div');
53
    $(div_relative).addClass( "autocomplete" );
54
    div_relative.append(inp);
55
    /* input doesn't have an elem after, add it to parent */
56
    if (next_elem_inp === null){
57
        parent_inp.append( div_relative );
58
    }  else {  // input has an elem after, add elem after it
59
        next_elem_inp.before(div_relative);
60
    };
61
    var currentFocus;
62
    /*execute a function when someone writes in the text field:*/
63
    var token_counter = 0;
64
    inp.addEventListener("input", function(e) {
65
        var a, val = this.value;
66
        /* var for async compare */
67
        var tmp_input = this.value.replace(/[+']/g, function(matched){
68
            return query_url_decode[matched];
69
        });
70
        token_counter++;
71
        currentFocus = -1;
72
        if (document.getElementsByClassName("autocomplete-items").length !== 0){
73
            a = document.getElementsByClassName("autocomplete-items")[0];
74
        } else {
75
            /*create a DIV element that will contain the items (values):*/
76
            a = document.createElement("DIV");
77
            a.setAttribute("id", this.id + "autocomplete-list");
78
            a.setAttribute("class", "autocomplete-items");
79
            /*append the DIV element as a child of the autocomplete container:*/
80
            this.parentNode.appendChild(a);
81
            /*append position absolute left/right:*/
82
            $(".autocomplete-items").css("left",left);
83
            $(".autocomplete-items").css("right",right);
84
        };
85
        /* get es_prefix key for builder */
86
        var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb);
87
        chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1');
88
        if (chose_prefix !== null){
89
            var prefix = es_prefix[chose_prefix].toString();
90
            val = val.replace(/[+']/g, function(matched){
91
                return query_url_encode[matched];
92
            });
93
            if (tmp_input == '' || tmp_input == null){
94
                closeAllLists();
95
                token_counter = 0;
96
            } else {
97
                $.ajax({
98
                    type: 'GET',
99
                    url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter,
100
                    success: function (data) {
101
                        //console.log(data);
102
                        if (data.length !== 0){
103
                            var myset; //Set for Autocomplete unique
104
                            /* autocomplete for all prefix */
105
                            if (chose_prefix === 'kw' || chose_prefix === ''){
106
                                myset = GetSetAutocompleteAllIdx(data, prefix, key);
107
                            } else { // autocomplete for one prefix
108
                                myset = GetSetAutocompleteOneIdx(data, prefix, key);
109
                            };
110
                            /* append set to autocomplete */
111
                            if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){
112
                                a.innerHTML = "";
113
                                for (let item of myset){
114
                                    a.appendChild(CreateDivItemAutocomplete(item, val));
115
                                };
116
                            };
117
                        } else {
118
                            closeAllLists(this);
119
                        };
120
                    },
121
                    error: function (data) {
122
                        console.log(data);
123
                    },
124
                });
125
            }
126
127
        };
128
    });
129
    /* get value for tag with name idx */
130
    function GetValueIdx(elem, nb){
131
        switch (elem[0].tagName){
132
            case 'INPUT':
133
                return elem[0].value;
134
            case 'SELECT':
135
                return select_idx[nb].options[select_idx[nb].selectedIndex].value;
136
            default:
137
                return null;
138
        };
139
    };
140
    /* get autocomplete for only one prefix title/author/etc... */
141
    function GetSetAutocompleteOneIdx(data, prefix, key){
142
        let myset = new Set();
143
        let tmp_data = data['hits']['hits'];
144
        for (let i = 0; i < tmp_data.length; i++) {
145
            for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){
146
                /* div with data for autocomplete */
147
                let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j];
148
                tmp = tmp.replace(/^\[/g, '');
149
                tmp = tmp.replace(/\]+$/g, '');
150
                myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, ''));
151
                if (myset.size >= nb_autocomplete) break;
152
            };
153
            if (myset.size >= nb_autocomplete) break;
154
        };
155
        return myset;
156
    };
157
    /* get autocomplete for all prefix */
158
    function GetSetAutocompleteAllIdx(data, prefix, key){
159
        let myset = new Set();
160
        var pref = prefix.split(",");
161
        for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length
162
            if (data[k] != '' && data[k] != null){
163
                let tmp_data = data[k]['hits']['hits'];
164
                for (i = 0; i < tmp_data.length; i++) {
165
                    for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){
166
                        /* div with data for autocomplete */
167
                        let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j]
168
                        myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, ''));
169
                        if (myset.size >= nb_autocomplete) break;
170
                    };
171
                    if (myset.size >= nb_autocomplete) break;
172
                };
173
                if (myset.size >= nb_autocomplete) break;
174
            }
175
        }
176
        return myset;
177
    };
178
179
    /*execute a function presses a key on the keyboard:*/
180
    inp.addEventListener("keydown", function(e) {
181
        var x = document.getElementById(this.id + "autocomplete-list");
182
        if (x) x = x.getElementsByTagName("div");
183
        if (e.keyCode == 40) { //DOWN
184
            /*If the arrow DOWN key is pressed,
185
            increase the currentFocus variable:*/
186
            currentFocus++;
187
            /*and and make the current item more visible:*/
188
            addActive(x);
189
        } else if (e.keyCode == 38) { //up
190
            /*If the arrow UP key is pressed,
191
            decrease the currentFocus variable:*/
192
            currentFocus--;
193
            /*and and make the current item more visible:*/
194
            addActive(x);
195
            e.preventDefault();
196
        } else if (e.keyCode == 13) {
197
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
198
            //e.preventDefault();
199
            if (currentFocus > -1) {
200
                /*and simulate a click on the "active" item:*/
201
                if (x) x[currentFocus].click();
202
            }
203
        }
204
        /* press Esc clear all autocomplete */
205
        else if (e.keyCode == 27) {
206
            closeAllLists();
207
        }
208
        /* press Esc clear all autocomplete */
209
        else if (e.keyCode == 8) {
210
            closeAllLists();
211
        };
212
    });
213
    function addActive(x) {
214
        /*a function to classify an item as "active":*/
215
        if (!x) return false;
216
        /*start by removing the "active" class on all items:*/
217
        removeActive(x);
218
        if (currentFocus >= x.length) currentFocus = 0;
219
        if (currentFocus < 0) currentFocus = (x.length - 1);
220
        /*add class "autocomplete-active":*/
221
        x[currentFocus].classList.add("autocomplete-active");
222
        inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim();
223
    };
224
    function removeActive(x) {
225
        /*a function to remove the "active" class from all autocomplete items:*/
226
        for (var i = 0; i < x.length; i++) {
227
            x[i].classList.remove("autocomplete-active");
228
        };
229
    };
230
231
    function closeAllLists(elem) {
232
        /*close all autocomplete lists in the document with class autocomplete-items */
233
        var x = document.getElementsByClassName("autocomplete-items");
234
        for (var i = 0; i < x.length; i++) {
235
            x[i].parentNode.removeChild(x[i])
236
        };
237
    };
238
239
    /* div for one item autocomplete */
240
    function CreateDivItemAutocomplete (elem){
241
        var b = document.createElement("DIV");
242
        // add element ";
243
        b.innerHTML += elem;
244
        /*insert a input field that will hold the current array item's value:*/
245
        b.innerHTML += "<input type='hidden' value=''>";
246
        /*execute a function when someone clicks on the item value (DIV element):*/
247
        b.addEventListener("click", function(e) {
248
            /* insert the value for the autocomplete text field: */
249
            inp.value = this.getElementsByTagName("input")[0].value;
250
            /* normalyzer hightlight without tags */
251
            //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim();
252
            inp.value = e.target.innerText;
253
            /* Submit form click mouse in div */
254
            this.closest("form").submit();
255
        });
256
        return b;
257
    };
258
259
    /*execute a function when someone clicks in the document:*/
260
    document.addEventListener("click", function (e) {
261
        closeAllLists(e.target);
262
    });
263
};
264
265
AutocompleteInitOpac();
(-)a/opac/svc/elasticsearch/opac-autocomplete.pl (-1 / +108 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use strict;
4
use warnings;
5
use CGI qw ( -utf8 );
6
use v5.10;
7
use JSON::XS;
8
use JSON;
9
use Koha::SearchEngine::Search;
10
use Switch;
11
use utf8;
12
use Text::Unaccent;
13
use CGI::Session;
14
15
my $searcher = Koha::SearchEngine::Search->new({index => 'biblios'});
16
my $cgi = CGI->new;
17
my $session = CGI::Session->new();
18
#name analyzer
19
$session->param(-name=>'analyzer', -value=>"autocomplete");
20
#GET request
21
$session->param(-name=>'prefix', -value=>$cgi->param("prefix"));
22
$session->param(-name=>'q', -value=>$cgi->param("q"));
23
$session->param(-name=>'key', -value=>$cgi->param("key"));
24
$session->param(-name=>'token_counter', -value=>$cgi->param("token_counter"));
25
$session->expire('+1h');
26
27
#Chose GET in key parametre
28
given ($session->param("key")) {
29
  #GET Autocomplete
30
  when ("autocomplete") {
31
    my @prefix = split /,/, $session->param("prefix");
32
    # how fields for autocomplete
33
    my $length = scalar @prefix;
34
    #search by many prefix fields
35
    if ($length > 1){
36
      print $cgi->header("application/json");
37
      print to_json(GetAutocompleteAllIdx($session->param("q"), \@prefix, $session->param("analyzer"), $session->param("token_counter"), $searcher));
38
    }
39
    #search by one prefix field
40
    elsif ($length == 1) {
41
      print $cgi->header("application/json");
42
      print to_json(GetAutocompleteOneIdx($session->param("q"), $prefix[0], $session->param("analyzer"), $session->param("token_counter"), $searcher));
43
    }
44
    #no prefix 404
45
    else {
46
      response404JSON();
47
    }
48
  }
49
  #no key 404
50
  default {
51
    response404JSON();
52
  }
53
}
54
#404 Error
55
sub response404JSON {
56
  my $json = JSON->new->utf8;
57
  my $header_type = "application/json";
58
  my $header_status = "404";
59
  my $output = $json->encode({
60
    "error" => "No data",
61
    "description" => "Bad request",
62
  });
63
  print $cgi->header(
64
    -type => $header_type,
65
    -charset => "utf-8",
66
    -status => $header_status
67
  );
68
  print $output;
69
  print "\n";
70
}
71
72
sub GetAutocompleteOneIdx {
73
  my ($cgi_q, $prefix, $analyzer, $token_counter, $searcher) = @_;
74
  my (%query, $results, @source);
75
  #prefix + analyzer
76
  my $prefix_analyzer = $prefix . '.' . $analyzer;
77
  # we can change this variables
78
  my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, ["<strong>"], ["</strong>"]);
79
  push(@source, $prefix);
80
  $query{'_source'} = \@source;
81
  $query{'query'}{'match'}{$prefix_analyzer}{'query'} = unac_string("UTF-8", $cgi_q);
82
  $query{'query'}{'match'}{$prefix_analyzer}{'operator'} = 'and';
83
  #hightlight
84
  $query{'highlight'}{'number_of_fragments'} = $nb_fragments;
85
  $query{'highlight'}{'fragment_size'} = $size_fragment;
86
  $query{'highlight'}{'pre_tags'} = $pre_tags;
87
  $query{'highlight'}{'post_tags'} = $post_tags;
88
  $query{'highlight'}{'fields'}{$prefix_analyzer} = {};
89
  $results = $searcher->search(\%query);
90
  $results->{'val'} = $cgi_q;
91
  $results->{'prefix'} = $prefix;
92
  $results->{'token_counter'} = $token_counter;
93
  return $results;
94
}
95
96
sub GetAutocompleteAllIdx {
97
  my ($cgi_q, $prefix, $analyzer, $token_counter, $searcher) = @_;
98
  my %results;
99
  my $idx = 0;
100
  foreach my $pref ( @$prefix ) {
101
      $results{$idx} = GetAutocompleteOneIdx($cgi_q, $pref, $analyzer, $token_counter, $searcher);
102
      $idx++;
103
  }
104
  $results{'val'} = $cgi_q;
105
  $results{'prefix'} = join( ',', @$prefix );
106
  $results{'token_counter'} = $token_counter;
107
  return \%results;
108
}

Return to bug 27113