Skip to content

Commit

Permalink
Refactor autocompleters to use query params
Browse files Browse the repository at this point in the history
  • Loading branch information
nimmolo committed Jun 18, 2024
1 parent ecc72d3 commit 046d888
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 93 deletions.
32 changes: 18 additions & 14 deletions app/classes/auto_complete.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
#
################################################################################

PUNCTUATION = '[ -\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]'

class AutoComplete
attr_accessor :string, :matches
attr_accessor :string, :matches, :all

PUNCTUATION = '[ -\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]'

def limit
1000
Expand All @@ -24,29 +24,25 @@ def self.subclass(type)
raise("Invalid auto-complete type: #{type.inspect}")
end

def initialize(string, _params = {})
self.string = string.to_s.strip_squeeze
def initialize(params = {})
self.string = params[:string].to_s.strip_squeeze
self.all = params[:all].present?
end

def matching_strings
# just use the first letter of the string to define the matches
self.matches = rough_matches(string[0])
clean_matches
minimal_string = refine_matches
return matches if all

minimal_string = refine_matches # defined in subclass
truncate_matches
[minimal_string] + matches
# [[minimal_string, nil]] + matches
end

private

def truncate_matches
return unless matches.length > limit

matches.slice!(limit..-1)
matches.push("...")
# matches.push(["...", nil])
end

def clean_matches
matches.map! do |str|
str.sub(/\s*[\r\n]\s*.*/m, "").sub(/\A\s+/, "").sub(/\s+\Z/, "")
Expand All @@ -57,4 +53,12 @@ def clean_matches
# end
matches.uniq!
end

def truncate_matches
return unless matches.length > limit

matches.slice!(limit..-1)
matches.push("...")
# matches.push(["...", nil])
end
end
4 changes: 2 additions & 2 deletions app/classes/auto_complete/for_location.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
class AutoComplete::ForLocation < AutoComplete::ByWord
attr_accessor :reverse

def initialize(string, params)
super(string, params)
def initialize(params)
super
self.reverse = (params[:format] == "scientific")
end

Expand Down
8 changes: 4 additions & 4 deletions app/classes/auto_complete/for_location_containing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

# Autocompleter for location names that encompass a given lat/lng.
class AutoComplete::ForLocationContaining < AutoComplete::ByWord
attr_accessor :reverse
attr_accessor :reverse, :lat, :lng

include Mappable::BoxMethods
# include Mappable::BoxMethods

def initialize(string, params)
super(string, params)
def initialize(params)
super
self.reverse = (params[:format] == "scientific")
self.lat = params[:lat]
self.lng = params[:lng]
Expand Down
18 changes: 8 additions & 10 deletions app/controllers/autocompleters_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,32 @@ class AutocompletersController < ApplicationController
# could add record ids. The first line of the returned results is the actual
# (minimal) string used to match the records. If it had to truncate the list
# of results, the last string is "...".
# type:: Type of string.
# id:: String user has entered.
# type:: Type of string.
# params[:string]:: String user has entered.
def new
@user = User.current = session_user

string = CGI.unescape(@id).strip_squeeze
if string.blank?
if params[:string].blank? && params[:all].blank?
render(json: ActiveSupport::JSON.encode([]))
else
render(json: ActiveSupport::JSON.encode(auto_complete_results(string)))
render(json: ActiveSupport::JSON.encode(auto_complete_results))
end
end

private

def auto_complete_results(string)
def auto_complete_results
case @type
when "location"
when "location", "location_containing"
params[:format] = @user&.location_format
when "herbarium"
params[:user_id] = @user&.id
end

::AutoComplete.subclass(@type).new(string, params).matching_strings
::AutoComplete.subclass(@type).new(params).matching_strings
end

# callback on `around_action`
def catch_ajax_errors
prepare_parameters
yield
Expand All @@ -53,8 +53,6 @@ def catch_ajax_errors

def prepare_parameters
@type = params[:type].to_s
@id = params[:id].to_s
@value = params[:value].to_s
end

def backtrace(exception)
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/forms_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def text_field_with_label(**args)
# our autocompleter: use the browser standard autocomplete att "one-time-code"
def autocompleter_field(**args)
autocompleter_args = {
placeholder: :start_typing.l, autocomplete: "one-time-code",
placeholder: :start_typing.l, autocomplete: "off",
data: { controller: :autocompleter, autocompleter_target: "input",
autocomplete: args[:autocomplete], separator: args[:separator] }
}.deep_merge(args.except(:autocomplete, :separator, :textarea))
Expand Down
77 changes: 42 additions & 35 deletions app/javascript/controllers/autocompleter_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DEFAULT_OPTS = {
// N = etc.
COLLAPSE: 0,
// where to request primer from
AJAX_URL: null,
AJAX_URL: "/autocompleters/new/",
// how long to wait before sending AJAX request (seconds)
REFRESH_DELAY: 0.10,
// how long to wait before hiding pulldown (seconds)
Expand All @@ -28,7 +28,7 @@ const DEFAULT_OPTS = {
// amount to move cursor on page up and down
PAGE_SIZE: 10,
// max length of string to send via AJAX
MAX_REQUEST_LINK: 50,
MAX_STRING_LENGTH: 50,
// Sub-match: starts finding new matches for the string *after the separator*
// allowed separators (e.g. " OR ")
SEPARATOR: null,
Expand All @@ -45,38 +45,29 @@ const DEFAULT_OPTS = {
// Allowed types of autocompleter. Sets some DEFAULT_OPTS from type
const AUTOCOMPLETER_TYPES = {
clade: {
AJAX_URL: "/autocompleters/new/clade/@",
},
herbarium: { // params[:user_id] handled in controller
AJAX_URL: "/autocompleters/new/herbarium/@",
UNORDERED: true
},
location: { // params[:format] handled in controller
AJAX_URL: "/autocompleters/new/location/@",
UNORDERED: true
},
location_containing: { // params encoded from dataset
AJAX_URL: "/autocompleters/new/location_containing/@",
ACT_LIKE_SELECT: true
},
name: {
AJAX_URL: "/autocompleters/new/name/@",
COLLAPSE: 1
},
project: {
AJAX_URL: "/autocompleters/new/project/@",
UNORDERED: true
},
region: {
AJAX_URL: "/autocompleters/new/location/@",
UNORDERED: true
},
species_list: {
AJAX_URL: "/autocompleters/new/species_list/@",
UNORDERED: true
},
user: {
AJAX_URL: "/autocompleters/new/user/@",
UNORDERED: true
}
}
Expand Down Expand Up @@ -108,6 +99,8 @@ const INTERNAL_OPTS = {

// Connects to data-controller="autocomplete"
export default class extends Controller {
// The select target is not the input element, but a <select> that can
// swap out the autocompleter type. The input element is the target.
static targets = ["input", "select"]

initialize() {
Expand Down Expand Up @@ -181,11 +174,13 @@ export default class extends Controller {
this.add_event_listeners();

// sanity check to show which autocompleter is currently on the element
this.inputTarget.setAttribute("data-ajax-url", this.AJAX_URL);
this.inputTarget.setAttribute("data-ajax-url", this.AJAX_URL + this.TYPE);

// If the primer is not based on input, go ahead and request from server.
if (this.ACT_LIKE_SELECT == true) {
this.inputTarget.click();
this.inputTarget.focus();
this.inputTarget.value = ' ';
}
}

Expand Down Expand Up @@ -744,9 +739,10 @@ export default class extends Controller {
// Update content of pulldown.
update_matches() {
this.verbose("update_matches()");

if (this.ACT_LIKE_SELECT)
this.current_row = 0;
// Remember which option used to be highlighted.
const last = this.current_row < 0 ? null : this.matches[this.current_row];
const _last = this.current_row < 0 ? null : this.matches[this.current_row];

// Update list of options appropriately.
if (this.ACT_LIKE_SELECT)
Expand All @@ -758,18 +754,22 @@ export default class extends Controller {
else
this.update_normal();

// Sort and remove duplicates.
this.matches = this.remove_dups(this.matches.sort());
// Sort and remove duplicates, unless it's already sorted.
if (!this.ACT_LIKE_SELECT)
this.matches = this.remove_dups(this.matches.sort());
// Try to find old highlighted row in new set of options.
this.update_current_row(last);
this.update_current_row(_last);
// Reset width each time we change the options.
this.current_width = this.inputTarget.offsetWidth;
}

// When "acting like a select" make it display all options in the
// order given right from the moment they enter the field.
// order given right from the moment they enter the field,
// and pick the first one.
update_select() {
this.matches = this.primer;
if (this.matches.length > 0)
this.inputTarget.value = this.matches[0];
}

// Grab all matches, doing exact match, ignoring number of words.
Expand Down Expand Up @@ -994,7 +994,7 @@ export default class extends Controller {
last_request = this.last_fetch_request;

// Don't make request on empty string!
if (!val || val.length < 1)
if (!this.ACT_LIKE_SELECT && (!val || val.length < 1))
return;

// Don't repeat last request accidentally!
Expand All @@ -1004,8 +1004,8 @@ export default class extends Controller {
// Memoize this condition, used twice.
// is the new search token an extension of the previous search string?
const new_val_refines_last_request =
(last_request.length < val.length) &&
(last_request == val.substr(0, last_request.length));
(last_request?.length < val.length) &&
(last_request == val.substr(0, last_request?.length));

// No need to make more constrained request if we got all results last time.
if (!this.last_fetch_incomplete &&
Expand All @@ -1019,35 +1019,42 @@ export default class extends Controller {
if (this.fetch_request && new_val_refines_last_request)
return;

if (val.length > this.MAX_STRING_LENGTH)
val = val.substr(0, this.MAX_STRING_LENGTH);

const _query_params = { string: val, ...this.request_params }
if (this.ACT_LIKE_SELECT) { _query_params["all"] = true; }
// Make request.
this.send_fetch_request(val, this.request_params);
this.send_fetch_request(_query_params);
}

// Send AJAX request for more matching strings.
async send_fetch_request(val, params) {
async send_fetch_request(query_params) {
this.verbose("send_fetch_request()");
if (val.length > this.MAX_REQUEST_LINK)
val = val.substr(0, this.MAX_REQUEST_LINK);

if (this.log) {
this.debug("Sending fetch request: " + val);
this.debug("Sending fetch request: " + query_params.string + "...");
}

// Need to doubly-encode this to prevent router from interpreting slashes,
// dots, etc.
const url = this.AJAX_URL.replace(
'@', encodeURIComponent(encodeURIComponent(val.replace(/\./g, '%2e')))
);
// const url = this.AJAX_URL.replace(
// '@', encodeURIComponent(encodeURIComponent(val.replace(/\./g, '%2e')))
// );
const _url = this.AJAX_URL + this.TYPE

this.last_fetch_request = val;
this.last_fetch_request = query_params.string;

const controller = new AbortController(),
signal = controller.signal;
const _controller = new AbortController();

if (this.fetch_request)
controller.abort();
_controller.abort();

const response = await get(url, { signal });
const response = await get(_url, {
signal: _controller.signal,
query: query_params,
responseKind: "json"
});
if (response.ok) {
const json = await response.json
if (json) {
Expand All @@ -1056,7 +1063,7 @@ export default class extends Controller {
}
} else {
this.fetch_request = null;
console.log(`got a ${response.status}`);
console.log(`got a ${response.status}: ${response.text}`);
}

}
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ def route_actions_hash
resources :articles, id: /\d+/

# ----- Autocompleters: fetch get ------------------------------------
get "/autocompleters/new/:type/:id", to: "autocompleters#new"
get "/autocompleters/new/:type", to: "autocompleters#new"

# ----- Checklist: just the show --------------------------------------
get "/checklist", to: "checklists#show"
Expand Down
Loading

0 comments on commit 046d888

Please sign in to comment.