diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-05 21:49:21 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-05 21:49:21 +0200 |
| commit | 1fa2f3821497d5529a6753cee84cf5ca679e8bcc (patch) | |
| tree | 7c6e98cfc9fc0b452e055c6201fb8aefa3c71965 | |
| parent | 9c630d186003a47563be2e3547485060dac7dd98 (diff) | |
Fix admin search results colliding with other search widgets
layouts/admin.html.erb's top-bar search results div shared id
"search_results" with nodes#new/nodes#edit's parent-search widget -
since the layout renders on every admin page, whichever widget's JS
ran last silently won the shared ID, putting top-bar results into the
parent-search box on affected pages. Renamed to "menu_search_results"
and updated admin_search.js to match.
Also modernizes admin_search.js's event bindings from keyup/keydown/
keypress/paste/cut + .attr("value") to input + .val() throughout
(menu_items, parent_search, move_to_search) - the multi-event binding
was working around old IE compatibility that .val() + "input" already
handles correctly. parent_search's result rendering now matches
menu_search's convention (real <p>/<a> markup with a .result_path
span) instead of a bare <a> in a throwaway wrapper div, so the same
CSS rule now correctly applies to both.
menu_search's JSON response gains node_path per result, matching what
admin_search's own results already provide - not yet consumed by
parent_search/move_to_search, which still render click-to-select links
rather than navigable ones (correct for their purpose - selecting a
value, not leaving the page).
Known remaining gap, not fixed here: menu_items, parent_search, and
move_to_search still all target the literal id "search_results"
between themselves. No live collision today since none of the three
currently share a page, but it's the same fragility as the bug above -
tracked alongside the existing menu_items/parent_search/move_to_search
consolidation backlog item rather than treated as resolved.
| -rw-r--r-- | app/controllers/admin_controller.rb | 33 | ||||
| -rw-r--r-- | app/views/layouts/admin.html.erb | 2 | ||||
| -rw-r--r-- | public/javascripts/admin_search.js | 105 |
3 files changed, 81 insertions, 59 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index d671384..e0098b0 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb | |||
| @@ -29,46 +29,49 @@ class AdminController < ApplicationController | |||
| 29 | .order("updated_at desc") | 29 | .order("updated_at desc") |
| 30 | .uniq.first(50) | 30 | .uniq.first(50) |
| 31 | end | 31 | end |
| 32 | 32 | ||
| 33 | def conventions | ||
| 34 | @node_kinds = CccConventions::NODE_KINDS | ||
| 35 | end | ||
| 36 | |||
| 33 | def search | 37 | def search |
| 34 | @results = Node.search params[:search_term], :per_page => 1000 | 38 | @results = Node.search params[:search_term], :per_page => 1000 |
| 35 | 39 | ||
| 36 | respond_to do |format| | 40 | respond_to do |format| |
| 37 | format.html do | 41 | format.html do |
| 38 | render :template => 'admin/search_results' | 42 | render :template => 'admin/search_results' |
| 39 | end | 43 | end |
| 40 | format.js do | 44 | format.js do |
| 41 | render( :json => @results.map do |node| | 45 | render( :json => @results.map do |node| |
| 42 | if node | 46 | if node |
| 43 | { :id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node) } | 47 | { :id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node) } |
| 44 | end | 48 | end |
| 45 | end | 49 | end |
| 46 | ) | 50 | ) |
| 47 | 51 | ||
| 48 | end | 52 | end |
| 49 | end | 53 | end |
| 50 | end | 54 | end |
| 51 | 55 | ||
| 52 | def menu_search | 56 | def menu_search |
| 53 | if params[:search_term] == "Root" | 57 | if params[:search_term] == "Root" |
| 54 | @results = [Node.root] | 58 | @results = [Node.root] |
| 55 | else | 59 | else |
| 56 | @results = Node.search params[:search_term] | 60 | @results = Node.search params[:search_term] |
| 57 | end | 61 | end |
| 58 | 62 | ||
| 59 | respond_to do |format| | 63 | respond_to do |format| |
| 60 | format.html do | 64 | format.html do |
| 61 | render :partial => 'admin/menu_search_results' | 65 | render :partial => 'admin/menu_search_results' |
| 62 | end | 66 | end |
| 63 | 67 | ||
| 64 | format.js do | 68 | format.js do |
| 65 | render( :json => @results.map do |node| | 69 | render( :json => @results.map do |node| |
| 66 | {:node_id => node.id, :title => node.title, :unique_name => node.unique_name} | 70 | {:node_id => node.id, :title => node.title, :unique_name => node.unique_name, :node_path => node_path(node)} |
| 67 | end | 71 | end |
| 68 | ) | 72 | ) |
| 69 | 73 | ||
| 70 | end | 74 | end |
| 71 | end | 75 | end |
| 72 | end | 76 | end |
| 73 | |||
| 74 | end | 77 | end |
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 4536693..a7ce68a 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb | |||
| @@ -49,7 +49,7 @@ | |||
| 49 | <span>Search: </span><%= text_field_tag :search_term, nil, autocomplete: "off" %> | 49 | <span>Search: </span><%= text_field_tag :search_term, nil, autocomplete: "off" %> |
| 50 | <% end %> | 50 | <% end %> |
| 51 | </div> | 51 | </div> |
| 52 | <div id="search_results" style="display: none"> | 52 | <div id="menu_search_results" style="display: none"> |
| 53 | 53 | ||
| 54 | </div> | 54 | </div> |
| 55 | </div> | 55 | </div> |
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js index 9bf878b..2565929 100644 --- a/public/javascripts/admin_search.js +++ b/public/javascripts/admin_search.js | |||
| @@ -36,8 +36,8 @@ admin_search = { | |||
| 36 | } | 36 | } |
| 37 | }); | 37 | }); |
| 38 | } else { | 38 | } else { |
| 39 | $('#search_results').slideUp(); | 39 | $('#menu_search_results').slideUp(); |
| 40 | $('#search_results').empty(); | 40 | $('#menu_search_results').empty(); |
| 41 | } | 41 | } |
| 42 | }); | 42 | }); |
| 43 | }, | 43 | }, |
| @@ -52,33 +52,33 @@ admin_search = { | |||
| 52 | }, | 52 | }, |
| 53 | 53 | ||
| 54 | show_results : function(results) { | 54 | show_results : function(results) { |
| 55 | $('#search_results').empty(); | 55 | $('#menu_search_results').empty(); |
| 56 | if (results.length) { | 56 | if (results.length) { |
| 57 | $('#search_results').append( | 57 | $('#menu_search_results').append( |
| 58 | "<p class='search_more'>Press Enter to see all results ⏎</p>" | 58 | "<p class='search_more'>Press Enter to see all results ⏎</p>" |
| 59 | ); | 59 | ); |
| 60 | } | 60 | } |
| 61 | for (result in results) { | 61 | for (result in results) { |
| 62 | $('#search_results').append( | 62 | $('#menu_search_results').append( |
| 63 | "<p><a href='" + results[result].node_path + "'>" + | 63 | "<p><a href='" + results[result].node_path + "'>" + |
| 64 | results[result].title + | 64 | results[result].title + |
| 65 | "<span class='result_path'>" + results[result].unique_name + "</span>" + | 65 | "<span class='result_path'>" + results[result].unique_name + "</span>" + |
| 66 | "</a></p>" | 66 | "</a></p>" |
| 67 | ); | 67 | ); |
| 68 | } | 68 | } |
| 69 | $('#search_results').slideDown(); | 69 | $('#menu_search_results').slideDown(); |
| 70 | } | 70 | } |
| 71 | }; | 71 | }; |
| 72 | 72 | ||
| 73 | menu_items = { | 73 | menu_items = { |
| 74 | 74 | ||
| 75 | initialize_search : function() { | 75 | initialize_search : function() { |
| 76 | $("#menu_search_term").bind("keyup", function() { | 76 | $("#menu_search_term").bind("input", function() { |
| 77 | if ($(this).attr("value")) { | 77 | if ($(this).val()) { |
| 78 | $.ajax({ | 78 | $.ajax({ |
| 79 | type: "GET", | 79 | type: "GET", |
| 80 | url: ADMIN_MENU_SEARCH_URL, | 80 | url: ADMIN_MENU_SEARCH_URL, |
| 81 | data: "search_term=" + $(this).attr("value"), | 81 | data: "search_term=" + $(this).val(), |
| 82 | dataType: "json", | 82 | dataType: "json", |
| 83 | success : function(results) { | 83 | success : function(results) { |
| 84 | menu_items.show_results(results); | 84 | menu_items.show_results(results); |
| @@ -125,12 +125,12 @@ parent_search = { | |||
| 125 | initialize_search : function() { | 125 | initialize_search : function() { |
| 126 | parent_search.initialize_radio_buttons(); | 126 | parent_search.initialize_radio_buttons(); |
| 127 | 127 | ||
| 128 | $("#parent_search_term").bind("keyup", function() { | 128 | $("#parent_search_term").bind("input", function() { |
| 129 | if ($(this).attr("value")) { | 129 | if ($(this).val()) { |
| 130 | $.ajax({ | 130 | $.ajax({ |
| 131 | type: "GET", | 131 | type: "GET", |
| 132 | url: ADMIN_MENU_SEARCH_URL, | 132 | url: ADMIN_MENU_SEARCH_URL, |
| 133 | data: "search_term=" + $(this).attr("value"), | 133 | data: "search_term=" + $(this).val(), |
| 134 | dataType: "json", | 134 | dataType: "json", |
| 135 | success : function(results) { | 135 | success : function(results) { |
| 136 | parent_search.show_results(results); | 136 | parent_search.show_results(results); |
| @@ -142,22 +142,31 @@ parent_search = { | |||
| 142 | $('#search_results').empty(); | 142 | $('#search_results').empty(); |
| 143 | } | 143 | } |
| 144 | }); | 144 | }); |
| 145 | |||
| 146 | $("#title").bind("input", function() { | ||
| 147 | parent_search.update_resulting_path(); | ||
| 148 | }); | ||
| 149 | |||
| 150 | $("#copy_resulting_path").bind("click", function() { | ||
| 151 | var path = $("#resulting_path").text(); | ||
| 152 | if (path === "—" || !navigator.clipboard) return; | ||
| 153 | navigator.clipboard.writeText(path); | ||
| 154 | }); | ||
| 145 | }, | 155 | }, |
| 146 | 156 | ||
| 147 | show_results : function(results) { | 157 | show_results : function(results) { |
| 148 | $("#search_results").empty(); | 158 | $("#search_results").empty(); |
| 149 | var found = false; | 159 | var found = false; |
| 150 | for (result in results) { | 160 | for (result in results) { |
| 151 | var link = $(("<a href='#'>"+ results[result].title + "</a>")); | ||
| 152 | $(link).bind("click", parent_search.link_closure(results[result])); | ||
| 153 | 161 | ||
| 162 | var link = $(( | ||
| 163 | "<p><a href='#'>" + results[result].title + | ||
| 164 | "<span class='result_path'>" + results[result].unique_name + "</span>" + | ||
| 165 | "</a></p>")); | ||
| 154 | 166 | ||
| 155 | // Sometimes I don't get jquery; wrap() didn't work *sigh* | 167 | $(link).bind("click", parent_search.link_closure(results[result])); |
| 156 | // Guess I'll need a book someday or another framework | ||
| 157 | var wrapper = $("<div></div>"); | ||
| 158 | $(wrapper).append(link); | ||
| 159 | 168 | ||
| 160 | $("#search_results").append(wrapper); | 169 | $("#search_results").append(link); |
| 161 | found = true; | 170 | found = true; |
| 162 | } | 171 | } |
| 163 | if (found) | 172 | if (found) |
| @@ -166,51 +175,61 @@ parent_search = { | |||
| 166 | 175 | ||
| 167 | link_closure : function(node) { | 176 | link_closure : function(node) { |
| 168 | var barf = function(){ | 177 | var barf = function(){ |
| 169 | $("#parent_search_term").attr("value", node.title); | 178 | $("#parent_search_term").val(node.title); |
| 170 | $("#parent_id").attr("value", node.node_id); | 179 | $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name); |
| 171 | $('#search_results').slideUp(); | 180 | $('#search_results').slideUp(); |
| 172 | $('#search_results').empty(); | 181 | $('#search_results').empty(); |
| 182 | parent_search.update_resulting_path(); | ||
| 173 | return false; | 183 | return false; |
| 174 | } | 184 | } |
| 175 | 185 | ||
| 176 | return barf; | 186 | return barf; |
| 177 | }, | 187 | }, |
| 178 | 188 | ||
| 179 | initialize_radio_buttons : function() { | 189 | update_resulting_path : function() { |
| 180 | $("#kind_top_level").bind("change", function(){ | 190 | var kind = $("input[name='kind']:checked"); |
| 181 | $("#parent_search_field").hide(); | 191 | var title = $("#title").val(); |
| 182 | }); | ||
| 183 | 192 | ||
| 184 | $("#kind_update").bind("change", function(){ | 193 | if (title === "") { |
| 185 | $("#parent_search_field").hide(); | 194 | $("#resulting_path").text("—"); |
| 186 | }); | 195 | return; |
| 196 | } | ||
| 187 | 197 | ||
| 188 | $("#kind_press_release").bind("change", function(){ | 198 | var prefix = kind.val() === "generic" |
| 189 | $("#parent_search_field").hide(); | 199 | ? ($("#parent_id").attr("data-unique-name") || "") |
| 190 | }); | 200 | : (kind.attr("data-path-prefix") || ""); |
| 191 | 201 | ||
| 192 | $("#kind_generic").bind("change", function(){ | 202 | clearTimeout(parent_search.path_timeout); |
| 193 | $("#parent_search_field").show(); | 203 | parent_search.path_timeout = setTimeout(function() { |
| 194 | }); | 204 | $.get("/nodes/parameterize_preview", { title: title }, function(slug) { |
| 205 | $("#resulting_path").text(window.location.origin + "/" + (prefix ? prefix + "/" : "") + slug); | ||
| 206 | }); | ||
| 207 | }, 300); | ||
| 208 | }, | ||
| 195 | 209 | ||
| 210 | initialize_radio_buttons : function() { | ||
| 211 | $("input[name='kind']").bind("change", function(){ | ||
| 212 | if ($(this).val() === "generic") { | ||
| 213 | $("#parent_search_field").show(); | ||
| 214 | } else { | ||
| 215 | $("#parent_search_field").hide(); | ||
| 216 | } | ||
| 217 | parent_search.update_resulting_path(); | ||
| 218 | }); | ||
| 196 | } | 219 | } |
| 197 | } | 220 | } |
| 198 | 221 | ||
| 199 | move_to_search = { | 222 | move_to_search = { |
| 200 | initialize_search : function() { | 223 | initialize_search : function() { |
| 201 | $("#move_to_search_term").bind("keyup", function() { move_to_search.do_search($(this))}); | 224 | $("#move_to_search_term").bind("input", function() {move_to_search.do_search($(this))}); |
| 202 | $("#move_to_search_term").bind("keydown", function() { move_to_search.do_search($(this))}); | ||
| 203 | $("#move_to_search_term").bind("keypress", function() { move_to_search.do_search($(this))}); | ||
| 204 | $("#move_to_search_term").bind("paste", function() { move_to_search.do_search($(this))}); | ||
| 205 | $("#move_to_search_term").bind("cut", function() { move_to_search.do_search($(this))}); | ||
| 206 | }, | 225 | }, |
| 207 | 226 | ||
| 208 | do_search : function(_this) { | 227 | do_search : function(_this) { |
| 209 | if (_this.attr("value")) { | 228 | if (_this.val()) { |
| 210 | $.ajax({ | 229 | $.ajax({ |
| 211 | type: "GET", | 230 | type: "GET", |
| 212 | url: ADMIN_MENU_SEARCH_URL, | 231 | url: ADMIN_MENU_SEARCH_URL, |
| 213 | data: "search_term=" + _this.attr("value"), | 232 | data: "search_term=" + _this.val(), |
| 214 | dataType: "json", | 233 | dataType: "json", |
| 215 | success : function(results) { | 234 | success : function(results) { |
| 216 | move_to_search.show_results(results); | 235 | move_to_search.show_results(results); |
| @@ -248,8 +267,8 @@ move_to_search = { | |||
| 248 | 267 | ||
| 249 | link_closure : function(node) { | 268 | link_closure : function(node) { |
| 250 | var barf = function(){ | 269 | var barf = function(){ |
| 251 | $("#move_to_search_term").attr("value", node.title); | 270 | $("#move_to_search_term").val(node.title); |
| 252 | $("#node_staged_parent_id").attr("value", node.node_id); | 271 | $("#node_staged_parent_id").val(node.node_id); |
| 253 | $('#search_results').slideUp(); | 272 | $('#search_results').slideUp(); |
| 254 | $('#search_results').empty(); | 273 | $('#search_results').empty(); |
| 255 | return false; | 274 | return false; |
