summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-12 13:58:36 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-12 13:58:36 +0200
commit19e0ee821d5b2b6d3397a81411f4f3a4cbd2fa83 (patch)
tree952551d861bb8348d436367e8f048c5f85853cf5
parent45bf65d04d046c0ea4a1150096b2a9b846d6c0b8 (diff)
Unify the four field-level pickers, fix search visibility
menu_items/parent_search/move_to_search/event_search each duplicated their own debounce-free AJAX call and result rendering. Replaced with one shared initSearchPicker, taking each picker's distinctive behavior (parent_search's live path preview, event_search's title hint) as a callback rather than a whole reimplementation. Adds a real debounce with an out-of-order-response guard -- none of the four had either before. admin_search (the Alt+F top-bar search) now shares the same function via its own url/isActive/header options, gaining the same guard and fixing an inconsistency of its own (it previously always slid its panel open, even on zero results). Each picker's results container gets its own id instead of sharing was ever supposed to be on screen at once, an assumption with no actual enforcement behind it. Styling moved from an id-pair (#menu_search_results, #search_results) to a shared .search_results class so a future picker never needs this file touched again. menu_search and the admin top-bar search now call Node.editor_search instead of the public, head-only Node.search -- both are admin-only, authenticated views, and had no reason to inherit the public search's "can't find a draft" limitation. The always-ignored :per_page => 1000 on the latter is gone too; Node.search's second argument was never read by the method at all. Also removed a stale #metadata a { text-transform: lowercase } rule, found while verifying the above -- written for the pre-subnav-removal expand-toggle, which no longer exists; it had been silently lowercasing nodes#edit's own, unrelated #metadata div (including move_to_search's results) by id coincidence ever since. #main_navigation and #overview_toggle intentionally left capitalized rather than special-cased -- both belong to the nav bar already slated to shrink to three icons, not worth polishing on the way out.
-rw-r--r--app/controllers/admin_controller.rb4
-rw-r--r--app/views/events/edit.html.erb4
-rw-r--r--app/views/events/new.html.erb2
-rw-r--r--app/views/layouts/admin.html.erb4
-rw-r--r--app/views/menu_items/edit.html.erb2
-rw-r--r--app/views/menu_items/new.html.erb4
-rw-r--r--app/views/nodes/edit.html.erb14
-rw-r--r--app/views/nodes/new.html.erb2
-rw-r--r--public/javascripts/admin_search.js351
-rw-r--r--public/stylesheets/admin.css13
10 files changed, 135 insertions, 265 deletions
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
index 435df57..3c45c49 100644
--- a/app/controllers/admin_controller.rb
+++ b/app/controllers/admin_controller.rb
@@ -31,7 +31,7 @@ class AdminController < ApplicationController
31 end 31 end
32 32
33 def search 33 def search
34 @results = Node.search params[:search_term], :per_page => 1000 34 @results = Node.editor_search(params[:search_term])
35 35
36 respond_to do |format| 36 respond_to do |format|
37 format.html do 37 format.html do
@@ -53,7 +53,7 @@ class AdminController < ApplicationController
53 if params[:search_term] == "Root" 53 if params[:search_term] == "Root"
54 @results = [Node.root] 54 @results = [Node.root]
55 else 55 else
56 @results = Node.search params[:search_term] 56 @results = Node.editor_search(params[:search_term])
57 end 57 end
58 58
59 respond_to do |format| 59 respond_to do |format|
diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb
index 6cb04bd..45b084f 100644
--- a/app/views/events/edit.html.erb
+++ b/app/views/events/edit.html.erb
@@ -19,12 +19,12 @@
19 <details> 19 <details>
20 <summary>Change node</summary> 20 <summary>Change node</summary>
21 <%= text_field_tag :event_node_search_term %> 21 <%= text_field_tag :event_node_search_term %>
22 <div id="search_results"></div> 22 <div id="event_search_results" class="search_results"></div>
23 <span class="field_hint">This will re-link the event to a different node — rarely needed.</span> 23 <span class="field_hint">This will re-link the event to a different node — rarely needed.</span>
24 </details> 24 </details>
25 <% else %> 25 <% else %>
26 <%= text_field_tag :event_node_search_term %> 26 <%= text_field_tag :event_node_search_term %>
27 <div id="search_results"></div> 27 <div id="event_search_results" class="search_results"></div>
28 <span class="field_hint">Optional — search and pick a node to associate this event with a page.</span> 28 <span class="field_hint">Optional — search and pick a node to associate this event with a page.</span>
29 <% end %> 29 <% end %>
30 <%= f.hidden_field :node_id %> 30 <%= f.hidden_field :node_id %>
diff --git a/app/views/events/new.html.erb b/app/views/events/new.html.erb
index b20fe48..7a1ee7a 100644
--- a/app/views/events/new.html.erb
+++ b/app/views/events/new.html.erb
@@ -10,7 +10,7 @@
10 <div class="node_description">Node</div> 10 <div class="node_description">Node</div>
11 <div class="node_content"> 11 <div class="node_content">
12 <%= text_field_tag :event_node_search_term %> 12 <%= text_field_tag :event_node_search_term %>
13 <div id="search_results"></div> 13 <div id="event_search_results" class="search_results"></div>
14 <%= f.hidden_field :node_id %> 14 <%= f.hidden_field :node_id %>
15 <span class="field_hint">Optional — search and pick a node to associate this event with a page.</span> 15 <span class="field_hint">Optional — search and pick a node to associate this event with a page.</span>
16 </div> 16 </div>
diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb
index e714c28..079346b 100644
--- a/app/views/layouts/admin.html.erb
+++ b/app/views/layouts/admin.html.erb
@@ -56,9 +56,7 @@
56 <span>Search: </span><%= text_field_tag :search_term, nil, autocomplete: "off" %> 56 <span>Search: </span><%= text_field_tag :search_term, nil, autocomplete: "off" %>
57 <% end %> 57 <% end %>
58 </div> 58 </div>
59 <div id="menu_search_results" style="display: none"> 59 <div id="menu_search_results" class="search_results" style="display: none"></div>
60
61 </div>
62 </div> 60 </div>
63 </body> 61 </body>
64</html> 62</html>
diff --git a/app/views/menu_items/edit.html.erb b/app/views/menu_items/edit.html.erb
index dc5e8f9..44e5a79 100644
--- a/app/views/menu_items/edit.html.erb
+++ b/app/views/menu_items/edit.html.erb
@@ -6,7 +6,7 @@
6 <div class="node_description">Search</div> 6 <div class="node_description">Search</div>
7 <div class="node_content"> 7 <div class="node_content">
8 <%= text_field_tag :menu_search_term %> 8 <%= text_field_tag :menu_search_term %>
9 <div id="search_results"></div> 9 <div id="menu_item_search_results" class="search_results"></div>
10 </div> 10 </div>
11 11
12 <div class="node_description">Node Id</div> 12 <div class="node_description">Node Id</div>
diff --git a/app/views/menu_items/new.html.erb b/app/views/menu_items/new.html.erb
index 68081d0..223cb8e 100644
--- a/app/views/menu_items/new.html.erb
+++ b/app/views/menu_items/new.html.erb
@@ -7,9 +7,7 @@
7 <td class="description">Search</td> 7 <td class="description">Search</td>
8 <td> 8 <td>
9 <%= text_field_tag :menu_search_term %> 9 <%= text_field_tag :menu_search_term %>
10 <div id="search_results"> 10 <div id="menu_item_search_results" class="search_results"></div>
11
12 </div>
13 </td> 11 </td>
14 </tr> 12 </tr>
15 <tr> 13 <tr>
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb
index 1c19410..13b78fc 100644
--- a/app/views/nodes/edit.html.erb
+++ b/app/views/nodes/edit.html.erb
@@ -45,14 +45,12 @@
45 <div class="node_description">parent</div> 45 <div class="node_description">parent</div>
46 <div class="node_content"> 46 <div class="node_content">
47 <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %> 47 <%= text_field_tag :move_to_search_term, @node.parent.title rescue "" %>
48 <div id="search_results"> 48 <div id="move_to_search_results" class="search_results"></div>
49 49 <%= f.hidden_field(
50 </div> 50 :staged_parent_id,
51 <%= f.hidden_field( 51 :value => @node.staged_parent_id || @node.parent_id
52 :staged_parent_id, 52 )
53 :value => @node.staged_parent_id || @node.parent_id 53 %>
54 )
55 %>
56 </div> 54 </div>
57 55
58 <%= fields_for @page do |d| %> 56 <%= fields_for @page do |d| %>
diff --git a/app/views/nodes/new.html.erb b/app/views/nodes/new.html.erb
index afc632f..bb7e078 100644
--- a/app/views/nodes/new.html.erb
+++ b/app/views/nodes/new.html.erb
@@ -34,7 +34,7 @@
34 <div class="node_content"> 34 <div class="node_content">
35 <%= text_field_tag :parent_search_term, @parent_name %> 35 <%= text_field_tag :parent_search_term, @parent_name %>
36 <%= hidden_field_tag :parent_id, @parent_id %> 36 <%= hidden_field_tag :parent_id, @parent_id %>
37 <div id="search_results"></div> 37 <div id="parent_search_results" class="search_results"></div>
38 </div> 38 </div>
39 </div> 39 </div>
40 40
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js
index 0e70845..f553334 100644
--- a/public/javascripts/admin_search.js
+++ b/public/javascripts/admin_search.js
@@ -1,5 +1,4 @@
1admin_search = { 1admin_search = {
2
3 initialize : function() { 2 initialize : function() {
4 $(document).bind("keydown", 'Alt+f', function(){ 3 $(document).bind("keydown", 'Alt+f', function(){
5 admin_search.display_toggle(); 4 admin_search.display_toggle();
@@ -20,25 +19,12 @@ admin_search = {
20 } 19 }
21 }); 20 });
22 21
23 $("#search_term").bind("input", function() { 22 initSearchPicker({
24 if (!$('#search_widget').is(':visible')) return; 23 inputSelector: "#search_term",
25 if ($(this).val()) { 24 resultsSelector: "#menu_search_results",
26 $.ajax({ 25 url: ADMIN_SEARCH_URL,
27 type: "GET", 26 isActive: function() { return $('#search_widget').is(':visible'); },
28 url: ADMIN_SEARCH_URL, 27 resultsHeaderHtml: "<p class='search_more'>Press Enter to see all results ⏎</p>"
29 data: "search_term=" + $(this).val(),
30 dataType: "json",
31 success: function(results) {
32 admin_search.show_results(results);
33 },
34 error: function(xhr, status, error) {
35 console.log("Ajax error:", status, error, xhr.status, xhr.responseText);
36 }
37 });
38 } else {
39 $('#menu_search_results').slideUp();
40 $('#menu_search_results').empty();
41 }
42 }); 28 });
43 }, 29 },
44 30
@@ -49,75 +35,97 @@ admin_search = {
49 $('#search_widget').fadeIn(); 35 $('#search_widget').fadeIn();
50 $('#search_term').focus(); 36 $('#search_term').focus();
51 } 37 }
52 },
53
54 show_results : function(results) {
55 $('#menu_search_results').empty();
56 if (results.length) {
57 $('#menu_search_results').append(
58 "<p class='search_more'>Press Enter to see all results ⏎</p>"
59 );
60 }
61 for (result in results) {
62 $('#menu_search_results').append(
63 "<p><a href='" + results[result].node_path + "'>" +
64 results[result].title +
65 "<span class='result_path'>" + results[result].unique_name + "</span>" +
66 "</a></p>"
67 );
68 }
69 $('#menu_search_results').slideDown();
70 } 38 }
71}; 39};
72 40
73menu_items = { 41function initSearchPicker(options) {
42 var inputSelector = options.inputSelector;
43 var resultsSelector = options.resultsSelector;
44 var url = options.url || ADMIN_MENU_SEARCH_URL;
45 var onSelect = options.onSelect; // omit for a real-navigation search like admin_search
46 var isActive = options.isActive; // optional guard, checked before every search
47 var resultsHeaderHtml = options.resultsHeaderHtml; // optional, prepended only when results exist
48 var requestId = 0;
49 var timeout;
74 50
75 initialize_search : function() { 51 $(inputSelector).bind("input", function() {
76 $("#menu_search_term").bind("input", function() { 52 if (isActive && !isActive()) return;
77 if ($(this).val()) {
78 $.ajax({
79 type: "GET",
80 url: ADMIN_MENU_SEARCH_URL,
81 data: "search_term=" + $(this).val(),
82 dataType: "json",
83 success : function(results) {
84 menu_items.show_results(results);
85 }
86 });
87 }
88 else {
89 $('#search_results').slideUp();
90 $('#search_results').empty();
91 }
92 });
93 },
94
95 show_results : function(results) {
96 $("#search_results").empty();
97 for (result in results) {
98 var link = $(("<a href='#'>"+ results[result].title + "</a>"));
99 $(link).bind("click", menu_items.link_closure(results[result]));
100 53
54 var term = $(this).val();
55 var results = $(resultsSelector);
56 clearTimeout(timeout);
101 57
102 // Sometimes I don't get jquery; wrap() didn't work *sigh* 58 if (!term) {
103 // Guess I'll need a book someday or another framework 59 results.slideUp();
104 var wrapper = $("<div></div>"); 60 results.empty();
105 $(wrapper).append(link) 61 return;
106
107 $("#search_results").append(wrapper);
108
109 } 62 }
110 },
111 63
112 link_closure : function(node) { 64 timeout = setTimeout(function() {
113 var barf = function(){ 65 var thisRequest = ++requestId;
114 $("#menu_item_node_id").val(node.node_id); 66 $.ajax({
115 $("#menu_item_path").val("/" + node.unique_name); 67 type: "GET",
116 $("#menu_item_title").val(node.title); 68 url: url,
117 return false; 69 data: "search_term=" + term,
118 } 70 dataType: "json",
71 success: function(nodes) {
72 if (thisRequest !== requestId) return;
73 results.empty();
74 if (nodes.length && resultsHeaderHtml) {
75 results.append(resultsHeaderHtml);
76 }
77 var found = false;
78 for (var i = 0; i < nodes.length; i++) {
79 (function(node) {
80 var link;
81 if (onSelect) {
82 link = $(
83 "<p><a href='#'>" + node.title +
84 "<span class='result_path'>" + node.unique_name + "</span>" +
85 "</a></p>"
86 );
87 link.find("a").bind("click", function() {
88 onSelect(node);
89 results.slideUp();
90 results.empty();
91 return false;
92 });
93 } else {
94 link = $(
95 "<p><a href='" + node.node_path + "'>" + node.title +
96 "<span class='result_path'>" + node.unique_name + "</span>" +
97 "</a></p>"
98 );
99 }
100 results.append(link);
101 found = true;
102 })(nodes[i]);
103 }
104 if (found) {
105 results.slideDown();
106 } else {
107 results.slideUp();
108 }
109 },
110 error: function(xhr, status, error) {
111 console.log("Ajax error:", status, error, xhr.status, xhr.responseText);
112 }
113 });
114 }, 250);
115 });
116}
119 117
120 return barf; 118menu_items = {
119 initialize_search : function() {
120 initSearchPicker({
121 inputSelector: "#menu_search_term",
122 resultsSelector: "#menu_item_search_results",
123 onSelect: function(node) {
124 $("#menu_item_node_id").val(node.node_id);
125 $("#menu_item_path").val("/" + node.unique_name);
126 $("#menu_item_title").val(node.title);
127 }
128 });
121 } 129 }
122}; 130};
123 131
@@ -125,21 +133,13 @@ parent_search = {
125 initialize_search : function() { 133 initialize_search : function() {
126 parent_search.initialize_radio_buttons(); 134 parent_search.initialize_radio_buttons();
127 135
128 $("#parent_search_term").bind("input", function() { 136 initSearchPicker({
129 if ($(this).val()) { 137 inputSelector: "#parent_search_term",
130 $.ajax({ 138 resultsSelector: "#parent_search_results",
131 type: "GET", 139 onSelect: function(node) {
132 url: ADMIN_MENU_SEARCH_URL, 140 $("#parent_search_term").val(node.title);
133 data: "search_term=" + $(this).val(), 141 $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name);
134 dataType: "json", 142 parent_search.update_resulting_path();
135 success : function(results) {
136 parent_search.show_results(results);
137 }
138 });
139 }
140 else {
141 $('#search_results').slideUp();
142 $('#search_results').empty();
143 } 143 }
144 }); 144 });
145 145
@@ -154,38 +154,6 @@ parent_search = {
154 }); 154 });
155 }, 155 },
156 156
157 show_results : function(results) {
158 $("#search_results").empty();
159 var found = false;
160 for (result in results) {
161
162 var link = $((
163 "<p><a href='#'>" + results[result].title +
164 "<span class='result_path'>" + results[result].unique_name + "</span>" +
165 "</a></p>"));
166
167 $(link).bind("click", parent_search.link_closure(results[result]));
168
169 $("#search_results").append(link);
170 found = true;
171 }
172 if (found)
173 $('#search_results').slideDown();
174 },
175
176 link_closure : function(node) {
177 var barf = function(){
178 $("#parent_search_term").val(node.title);
179 $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name);
180 $('#search_results').slideUp();
181 $('#search_results').empty();
182 parent_search.update_resulting_path();
183 return false;
184 }
185
186 return barf;
187 },
188
189 update_resulting_path : function() { 157 update_resulting_path : function() {
190 var kind = $("input[name='kind']:checked"); 158 var kind = $("input[name='kind']:checked");
191 var title = $("#title").val(); 159 var title = $("#title").val();
@@ -223,122 +191,35 @@ parent_search = {
223 $("#parent_search_field").hide(); 191 $("#parent_search_field").hide();
224 } 192 }
225 } 193 }
226} 194};
227 195
228move_to_search = { 196move_to_search = {
229 initialize_search : function() { 197 initialize_search : function() {
230 $("#move_to_search_term").bind("input", function() {move_to_search.do_search($(this))}); 198 initSearchPicker({
231 }, 199 inputSelector: "#move_to_search_term",
232 200 resultsSelector: "#move_to_search_results",
233 do_search : function(_this) { 201 onSelect: function(node) {
234 if (_this.val()) { 202 $("#move_to_search_term").val(node.title);
235 $.ajax({ 203 $("#node_staged_parent_id").val(node.node_id);
236 type: "GET", 204 }
237 url: ADMIN_MENU_SEARCH_URL, 205 });
238 data: "search_term=" + _this.val(),
239 dataType: "json",
240 success : function(results) {
241 move_to_search.show_results(results);
242 }
243 });
244 }
245 else {
246 $('#search_results').slideUp();
247 $('#search_results').empty();
248 }
249 },
250
251 show_results : function(results) {
252 $("#search_results").empty();
253 var found = false;
254 for (result in results) {
255 var link = $(("<a href='#'>"+ results[result].title + "</a>"));
256 $(link).bind("click", move_to_search.link_closure(results[result]));
257
258
259 // Sometimes I don't get jquery; wrap() didn't work *sigh*
260 // Guess I'll need a book someday or another framework
261 var wrapper = $("<div></div>");
262 $(wrapper).append(link)
263
264 $("#search_results").append(wrapper);
265 found = true;
266 }
267 if (found)
268 $('#search_results').slideDown();
269 else
270 $('#search_results').slideUp();
271
272 },
273
274 link_closure : function(node) {
275 var barf = function(){
276 $("#move_to_search_term").val(node.title);
277 $("#node_staged_parent_id").val(node.node_id);
278 $('#search_results').slideUp();
279 $('#search_results').empty();
280 return false;
281 }
282
283 return barf;
284 } 206 }
285} 207};
286 208
287event_search = { 209event_search = {
288 initialize_search : function() { 210 initialize_search : function() {
289 $("#event_node_search_term").bind("input", function() { 211 initSearchPicker({
290 if ($(this).val()) { 212 inputSelector: "#event_node_search_term",
291 $.ajax({ 213 resultsSelector: "#event_search_results",
292 type: "GET", 214 onSelect: function(node) {
293 url: ADMIN_MENU_SEARCH_URL, 215 $("#event_node_search_term").val(node.title);
294 data: "search_term=" + $(this).val(), 216 $("#event_node_id").val(node.node_id);
295 dataType: "json",
296 success : function(results) {
297 event_search.show_results(results);
298 }
299 });
300 }
301 else {
302 $('#search_results').slideUp();
303 $('#search_results').empty();
304 }
305 });
306 },
307 217
308 show_results : function(results) { 218 var title_field = $("#event_title");
309 $("#search_results").empty(); 219 if (title_field.val() === "") {
310 var found = false; 220 $("#event_title_hint").text("Using \"" + node.title + "\" from the associated node.");
311 for (result in results) { 221 }
312 var link = $((
313 "<p><a href='#'>" + results[result].title +
314 "<span class='result_path'>" + results[result].unique_name + "</span>" +
315 "</a></p>"));
316
317 $(link).bind("click", event_search.link_closure(results[result]));
318
319 $("#search_results").append(link);
320 found = true;
321 }
322 if (found)
323 $('#search_results').slideDown();
324 else
325 $('#search_results').slideUp();
326 },
327
328 link_closure : function(node) {
329 var barf = function(){
330 $("#event_node_search_term").val(node.title);
331 $("#event_node_id").val(node.node_id);
332 $('#search_results').slideUp();
333 $('#search_results').empty();
334
335 var title_field = $("#event_title");
336 if (title_field.val() === "") {
337 $("#event_title_hint").text("Using \"" + node.title + "\" from the associated node.");
338 } 222 }
339 223 });
340 return false;
341 }
342 return barf;
343 } 224 }
344}; 225};
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 5c1e489..da31535 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -155,7 +155,6 @@ input[type=radio] {
155 padding-right: 5px; 155 padding-right: 5px;
156 padding-top: 1px; 156 padding-top: 1px;
157 padding-bottom: 1px; 157 padding-bottom: 1px;
158 text-transform: lowercase;
159} 158}
160 159
161#metadata a:hover { 160#metadata a:hover {
@@ -1023,22 +1022,19 @@ div#draft_list table td.actions a {
1023 font-size: 18px; 1022 font-size: 18px;
1024} 1023}
1025 1024
1026#menu_search_results p, 1025.search_results p {
1027#search_results p {
1028 margin: 0; 1026 margin: 0;
1029 padding: 4px 4px 0 4px; 1027 padding: 4px 4px 0 4px;
1030 border-bottom: 1px solid #e8e8e8; 1028 border-bottom: 1px solid #e8e8e8;
1031} 1029}
1032 1030
1033#menu_search_results p a, 1031.search_results p a {
1034#search_results p a {
1035 display: block; 1032 display: block;
1036 font-weight: bold; 1033 font-weight: bold;
1037 font-size: 0.95rem; 1034 font-size: 0.95rem;
1038} 1035}
1039 1036
1040#menu_search_results p span.result_path, 1037.search_results p span.result_path {
1041#search_results p span.result_path {
1042 display: block; 1038 display: block;
1043 margin-top: 2px; 1039 margin-top: 2px;
1044 font-size: 0.75rem; 1040 font-size: 0.75rem;
@@ -1047,8 +1043,7 @@ div#draft_list table td.actions a {
1047 padding-bottom: 4px; 1043 padding-bottom: 4px;
1048} 1044}
1049 1045
1050#menu_search_results p.search_more, 1046.search_results p.search_more {
1051#search_results p.search_more {
1052 margin: 0; 1047 margin: 0;
1053 padding: 6px 4px; 1048 padding: 6px 4px;
1054 font-size: 0.8rem; 1049 font-size: 0.8rem;