From 5aec6e9a82507f7e674c27ad4da513e31af81d40 Mon Sep 17 00:00:00 2001
From: Mikk3lRo <mikk3lro@gmail.com>
Date: Tue, 3 Oct 2017 00:55:47 +0200
Subject: [PATCH] Add new option: search_word_boundary

---
 coffee/lib/abstract-chosen.coffee |  3 ++-
 public/options.html               |  5 +++++
 spec/jquery/searching.spec.coffee | 18 ++++++++++++++++++
 spec/proto/searching.spec.coffee  | 19 +++++++++++++++++++
 4 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/coffee/lib/abstract-chosen.coffee b/coffee/lib/abstract-chosen.coffee
index a83c194f826..3b85fb7b0b5 100644
--- a/coffee/lib/abstract-chosen.coffee
+++ b/coffee/lib/abstract-chosen.coffee
@@ -27,6 +27,7 @@ class AbstractChosen
     @enable_split_word_search = if @options.enable_split_word_search? then @options.enable_split_word_search else true
     @group_search = if @options.group_search? then @options.group_search else true
     @search_contains = @options.search_contains || false
+    @search_word_boundary = if @options.search_word_boundary? then @options.search_word_boundary else '^|\\s|\\b'
     @single_backstroke_delete = if @options.single_backstroke_delete? then @options.single_backstroke_delete else true
     @max_selected_options = @options.max_selected_options || Infinity
     @inherit_select_classes = @options.inherit_select_classes || false
@@ -217,7 +218,7 @@ class AbstractChosen
       this.winnow_results_set_highlight()
 
   get_search_regex: (escaped_search_string) ->
-    regex_string = if @search_contains then escaped_search_string else "(^|\\s|\\b)#{escaped_search_string}[^\\s]*"
+    regex_string = if @search_contains then escaped_search_string else "(#{@search_word_boundary})#{escaped_search_string}[^\\s]*"
     regex_string = "^#{regex_string}" unless @enable_split_word_search or @search_contains
     regex_flag = if @case_sensitive_search then "" else "i"
     new RegExp(regex_string, regex_flag)
diff --git a/public/options.html b/public/options.html
index 6a92bead277..2f47775511c 100644
--- a/public/options.html
+++ b/public/options.html
@@ -91,6 +91,11 @@ <h3>Example:</h3>
           <td>true</td>
           <td>By default, Chosen will search group labels as well as options, and filter to show all options below matching groups. Set this to <code class="language-javascript">false</code> to search only in the options.</td>
         </tr>
+        <tr>
+          <td>search_word_boundary</td>
+          <td>^|\\b|\\s</td>
+          <td>By default, Chosen uses JS RegExp's built-in word boundary to detect word beginnings as well as whitespace or the beginning of the entire label. That works great for ascii-only languages, but <strong>will</strong> erroneously detect word boundaries after letters with umlauts among many, many others.<br>You can pass a string (that will be interpreted as part of a <code class="language-javascript">RegExp</code>) refined for your language and use case to correctly detect word boundaries. A (simplified) example could be <code class="language-javascript">'^|[^A-zæøåÆØÅ]'</code> for Danish.</td>
+        </tr>
         <tr>
           <td>single_backstroke_delete</td>
           <td>true</td>
diff --git a/spec/jquery/searching.spec.coffee b/spec/jquery/searching.spec.coffee
index d17f62f85bc..dd4f4f19f74 100644
--- a/spec/jquery/searching.spec.coffee
+++ b/spec/jquery/searching.spec.coffee
@@ -279,3 +279,21 @@ describe "Searching", ->
         search_field.trigger("keyup")
         expect(div.find(".active-result").length).toBe(1)
         expect(div.find(".active-result")[0].innerText.slice(1)).toBe(boundary_thing)
+
+  it "respects custom search_word_boundary when not using search_contains", ->
+    div = $("<div>").html("""
+      <select>
+        <option value="Frank Møller">Frank Møller</option>
+      </select>
+    """)
+    div.find("select").chosen({search_word_boundary: '^|[^A-zæøåÆØÅ]'})
+    div.find(".chosen-container").trigger("mousedown") # open the drop
+
+    search_field = div.find(".chosen-search-input")
+    search_field.val('ller')
+    search_field.trigger("keyup")
+    expect(div.find(".active-result").length).toBe(0)
+    search_field.val('Møl')
+    search_field.trigger("keyup")
+    expect(div.find(".active-result").length).toBe(1)
+    expect(div.find(".active-result")[0].innerHTML).toBe('Frank <em>Møl</em>ler')
diff --git a/spec/proto/searching.spec.coffee b/spec/proto/searching.spec.coffee
index 457396b95d2..7980ac6bd1b 100644
--- a/spec/proto/searching.spec.coffee
+++ b/spec/proto/searching.spec.coffee
@@ -291,3 +291,22 @@ describe "Searching", ->
         simulant.fire(search_field, "keyup")
         expect(div.select(".active-result").length).toBe(1)
         expect(div.select(".active-result")[0].innerText.slice(1)).toBe(boundary_thing)
+        
+  it "respects custom search_word_boundary when not using search_contains", ->
+    div = new Element("div")
+    div.update("""
+      <select>
+        <option value="Frank Møller">Frank Møller</option>
+      </select>
+    """)
+    new Chosen(div.down("select"), {search_word_boundary: '^|[^A-zæøåÆØÅ]'})
+    simulant.fire(div.down(".chosen-container"), "mousedown") # open the drop
+
+    search_field = div.down(".chosen-search-input")
+    search_field.value = 'ller'
+    simulant.fire(search_field, "keyup")
+    expect(div.select(".active-result").length).toBe(0)
+    search_field.value = 'Møl'
+    simulant.fire(search_field, "keyup")
+    expect(div.select(".active-result").length).toBe(1)
+    expect(div.select(".active-result")[0].innerHTML).toBe('Frank <em>Møl</em>ler')