Skip to content

Commit

Permalink
Merge pull request #2293 from MushroomObserver/recent-stimulus-changes
Browse files Browse the repository at this point in the history
`map`, `geocode`, `autocompleter` Stimulus refactor from `actions` to `outlets`
  • Loading branch information
nimmolo authored Aug 8, 2024
2 parents 06aba12 + db417aa commit 99db4e9
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 249 deletions.
170 changes: 170 additions & 0 deletions app/helpers/autocompleter_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# frozen_string_literal: true

# rubocop:disable Metrics/AbcSize
module AutocompleterHelper
# MO's autocompleter_field is a text_field that fetches suggestions from the
# db for the requested model. (For a textarea, pass textarea: true.)
#
# The stimulus controller handles keyboard and mouse interactions, does the
# fetching, and draws the dropdown menu. `args` allow incoming data attributes
# to deep_merge with controller data.
#
# We attempt to disable browser autocomplete via `autocomplete="off"` — the
# W3C standard API, but it has never been honored by Chrome or Safari. Chrome
# seems to be in a race to defeat the evolving hacks by developers to disable
# inappropriate autocompletes, and Safari is not much better - you just can't
# turn their crap off. (documented on SO)
#
def autocompleter_field(**args)
ac_args = {
placeholder: :start_typing.l, autocomplete: "off",
data: { autocompleter_target: "input" }
}.deep_merge(args.except(*autocompleter_outer_args))
ac_args[:class] = class_names("dropdown", args[:class])
ac_args[:wrap_data] = autocompleter_wrap_data(args)
ac_args[:label_after] = autocompleter_label_after(args)
ac_args[:label_end] = autocompleter_label_end(args)
ac_args[:append] = autocompleter_append(args)

if args[:textarea] == true
text_area_with_label(**ac_args)
else
text_field_with_label(**ac_args)
end
end

# Any arg not on this list gets sent to the text field/area.
def autocompleter_outer_args
[:wrap_data, :type, :separator, :textarea, :hidden_value, :hidden_data,
:create_text, :keep_text, :edit_text, :find_text, :create, :create_path,
:map_outlet, :geocode_outlet]
end

def autocompleter_wrap_data(args)
{
controller: :autocompleter, type: args[:type],
separator: args[:separator],
autocompleter_map_outlet: args[:map_outlet],
autocompleter_geocode_outlet: args[:geocode_outlet],
autocompleter_target: "wrap"
}.deep_merge(args[:wrap_data] || {})
end

def autocompleter_label_after(args)
capture do
[
autocompleter_has_id_indicator,
autocompleter_find_button(args),
autocompleter_keep_button(args),
autocompleter_hidden_field(**args)
].safe_join
end
end

def autocompleter_label_end(args)
capture do
concat(autocompleter_create_button(args))
concat(autocompleter_modal_create_link(args))
end
end

def autocompleter_append(args)
capture do
concat(autocompleter_dropdown)
concat(args[:append])
end
end

def autocompleter_has_id_indicator
link_icon(:check, title: :autocompleter_has_id.l,
class: "ml-3 px-2 text-success has-id-indicator",
data: { autocompleter_target: "hasIdIndicator" })
end

def autocompleter_create_button(args)
return if !args[:create_text] || args[:create].present?

icon_link_to(
args[:create_text], "#",
icon: :plus, show_text: true, icon_class: "text-primary",
name: "create_#{args[:type]}", class: "ml-3 create-button",
data: { autocompleter_target: "createBtn",
action: "autocompleter#swapCreate:prevent" }
)
end

def autocompleter_modal_create_link(args)
return unless args[:create_text] && args[:create].present? &&
args[:create_path].present?

modal_link_to(
args[:create], args[:create_text], args[:create_path],
icon: :plus, show_text: true, icon_class: "text-primary",
name: "create_#{args[:type]}", class: "ml-3 create-link",
data: { autocompleter_target: "createBtn" }
)
end

def autocompleter_find_button(args)
return unless args[:find_text]

icon_link_to(
args[:find_text], "#",
icon: :find_on_map, show_text: false, icon_class: "text-primary",
name: "find_#{args[:type]}", class: "ml-3 d-none",
data: { map_target: "showBoxBtn",
action: "map#showBox:prevent" }
)
end

def autocompleter_keep_button(args)
return unless args[:keep_text]

icon_link_to(
args[:keep_text], "#",
icon: :apply, show_text: false, icon_class: "text-primary",
active_icon: :edit, active_content: args[:edit_text],
name: "keep_#{args[:type]}", class: "ml-3 d-none",
data: { autocompleter_target: "keepBtn", map_target: "lockBoxBtn",
action: "map#toggleBoxLock:prevent" }
)
end

# minimum args :form, :type.
# Send :hidden to fill the id, :hidden_data to merge with hidden field data
def autocompleter_hidden_field(**args)
return unless args[:form].present? && args[:type].present?

model = autocompleter_type_to_model(args[:type])
data = { autocompleter_target: "hidden" }.merge(args[:hidden_data] || {})
args[:form].hidden_field(:"#{model}_id", value: args[:hidden_value], data:)
end

def autocompleter_type_to_model(type)
case type
when :region
:location
when :clade
:name
else
type
end
end

def autocompleter_dropdown
tag.div(class: "auto_complete dropdown-menu",
data: { autocompleter_target: "pulldown",
action: "scroll->autocompleter#scrollList:passive" }) do
tag.ul(class: "virtual_list", data: { autocompleter_target: "list" }) do
10.times do |i|
concat(tag.li(class: "dropdown-item") do
link_to("", "#", data: {
row: i, action: "click->autocompleter#selectRow:prevent"
})
end)
end
end
end
end
end
# rubocop:enable Metrics/AbcSize
135 changes: 2 additions & 133 deletions app/helpers/forms_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# helpers for form tags
# rubocop:disable Metrics/ModuleLength
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity
module FormsHelper
# Bootstrap submit button
Expand Down Expand Up @@ -167,7 +166,7 @@ def text_field_with_label(**args)
label_opts = field_label_opts(args)
label_opts[:class] = class_names(label_opts[:class], args[:label_class])

tag.div(class: wrap_class, data: wrap_data) do
tag.div(class: wrap_class, data: wrap_data, id: args[:wrap_id]) do
concat(text_label_row(args, label_opts))
if args[:addon].present? # text addon, not interactive
concat(tag.div(class: "input-group") do
Expand Down Expand Up @@ -225,135 +224,6 @@ def text_label_row(args, label_opts)
end
end

# MO's autocompleter_field is a text_field that fetches suggestions from the
# db for the requested model. (For a textarea, pass textarea: true.) The
# stimulus controller handles keyboard and mouse interactions, does the
# fetching, and draws the dropdown menu. `args` allow incoming data attributes
# to deep_merge with controller data. We attempt to disable browser
# autocomplete via `autocomplete="off"` — the W3C standard API, but it
# has never been honored by Chrome or Safari. Chrome seems to be in a race to
# defeat the evolving hacks by developers to disable inappropriate
# autocompletes, and Safari is not much better - you just can't turn their
# crap off. (documented on SO)
#
def autocompleter_field(**args)
ac_args = {
placeholder: :start_typing.l, autocomplete: "off",
data: { autocompleter_target: "input" }
}.deep_merge(args.except(:wrap_data, :type, :separator, :textarea,
:hidden_value, :hidden_data, :create_text,
:keep_text, :edit_text, :find_text))
ac_args[:class] = class_names("dropdown", args[:class])
ac_args[:wrap_data] = {
controller: :autocompleter, type: args[:type],
separator: args[:separator],
autocompleter_map_outlet: ".map-outlet",
autocompleter_geocode_outlet: ".geocode-outlet",
autocompleter_target: "wrap"
}.deep_merge(args[:wrap_data] || {})
ac_args[:label_after] = capture do
[
autocompleter_has_id_indicator,
autocompleter_find_button(args),
autocompleter_keep_button(args),
autocompleter_hidden_field(**args)
].safe_join
end
ac_args[:label_end] = capture do
autocompleter_create_button(args)
end
ac_args[:append] = capture do
concat(autocompleter_dropdown)
concat(args[:append])
end

if args[:textarea] == true
text_area_with_label(**ac_args)
else
text_field_with_label(**ac_args)
end
end

def autocompleter_has_id_indicator
link_icon(:check, title: :autocompleter_has_id.l,
class: "ml-3 px-2 text-success has-id-indicator",
data: { autocompleter_target: "hasIdIndicator" })
end

def autocompleter_create_button(args)
return unless args[:create_text]

icon_link_to(
args[:create_text], "#",
icon: :plus, show_text: true, icon_class: "text-primary",
name: "create_#{args[:type]}", class: "ml-3 create-button",
data: { autocompleter_target: "createBtn",
action: "autocompleter#swapCreate:prevent" }
)
end

def autocompleter_find_button(args)
return unless args[:find_text]

icon_link_to(
args[:find_text], "#",
icon: :find_on_map, show_text: false, icon_class: "text-primary",
name: "find_#{args[:type]}", class: "ml-3 d-none",
data: { map_target: "showBoxBtn",
action: "map#showBox:prevent" }
)
end

def autocompleter_keep_button(args)
return unless args[:keep_text]

icon_link_to(
args[:keep_text], "#",
icon: :apply, show_text: false, icon_class: "text-primary",
active_icon: :edit, active_content: args[:edit_text],
name: "keep_#{args[:type]}", class: "ml-3 d-none",
data: { autocompleter_target: "keepBtn", map_target: "lockBoxBtn",
action: "map#toggleBoxLock:prevent" }
)
end

# minimum args :form, :type.
# Send :hidden to fill the id, :hidden_data to merge with hidden field data
def autocompleter_hidden_field(**args)
return unless args[:form].present? && args[:type].present?

model = autocompleter_type_to_model(args[:type])
data = { autocompleter_target: "hidden" }.merge(args[:hidden_data] || {})
args[:form].hidden_field(:"#{model}_id", value: args[:hidden_value], data:)
end

def autocompleter_type_to_model(type)
case type
when :region
:location
when :clade
:name
else
type
end
end

def autocompleter_dropdown
tag.div(class: "auto_complete dropdown-menu",
data: { autocompleter_target: "pulldown",
action: "scroll->autocompleter#scrollList:passive" }) do
tag.ul(class: "virtual_list", data: { autocompleter_target: "list" }) do
10.times do |i|
concat(tag.li(class: "dropdown-item") do
link_to("", "#", data: {
row: i, action: "click->autocompleter#selectRow:prevent"
})
end)
end
end
end
end

# Bootstrap select.
# Works for select_year but not date_select, which generates multiple selects
def select_with_label(**args)
Expand Down Expand Up @@ -672,13 +542,12 @@ def separate_field_options_from_args(args, extras = [])
exceptions = [
:form, :field, :label, :class, :width, :inline, :between, :label_after,
:label_end, :append, :help, :addon, :optional, :required, :monospace,
:type, :wrap_data, :button, :button_data
:type, :wrap_data, :wrap_id, :button, :button_data
] + extras

args.clone.except(*exceptions)
end
end
# rubocop:enable Metrics/ModuleLength
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/CyclomaticComplexity
1 change: 1 addition & 0 deletions app/helpers/map_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def make_map(objects: [], **args)
controller: "map",
map_target: "mapDiv",
map_type: "info",
map_open: true,
editable: false,
controls: [:large_map, :map_type].to_json,
location_format: User.current_location_format # method has a default
Expand Down
Loading

0 comments on commit 99db4e9

Please sign in to comment.