summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
Diffstat (limited to 'public')
-rw-r--r--public/javascripts/admin_interface.js253
-rw-r--r--public/javascripts/admin_search.js393
-rw-r--r--public/javascripts/cacycle_diff.js1112
-rw-r--r--public/javascripts/related_assets.js80
-rw-r--r--public/stylesheets/admin.css1150
-rw-r--r--public/stylesheets/ccc.css24
6 files changed, 1418 insertions, 1594 deletions
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js
index 450438f..e3d4ff1 100644
--- a/public/javascripts/admin_interface.js
+++ b/public/javascripts/admin_interface.js
@@ -12,7 +12,7 @@ function hide_all() {
12 12
13$(document).ready(function () { 13$(document).ready(function () {
14 admin_search.initialize(); 14 admin_search.initialize();
15 15
16 tinymce.init({ 16 tinymce.init({
17 selector: 'textarea.with_editor', 17 selector: 'textarea.with_editor',
18 license_key: 'gpl', 18 license_key: 'gpl',
@@ -29,40 +29,56 @@ $(document).ready(function () {
29 cccms.setup_autosave(); 29 cccms.setup_autosave();
30 }); 30 });
31 } 31 }
32 }); 32 });
33
34 if ($("#dashboard_search_term").length != 0) {
35 dashboard_search.initialize();
36 }
33 37
34 if ($("#menu_search_term").length != 0) { 38 if ($("#menu_search_term").length != 0) {
35 menu_items.initialize_search(); 39 menu_items.initialize_search();
36 } 40 }
37 41
38 if ($("#menu_item_list").length != 0) { 42 if ($("#menu_item_list").length != 0) {
39 menu_item_sorter.initialize(); 43 menu_item_sorter.initialize();
40 } 44 }
41 45
42 if ($("#metadata").length != 0) { 46 if ($("#metadata").length != 0) {
43 meta_data.initialize(); 47 meta_data.initialize();
44 } 48 }
45 49
46 if ($("#parent_search_term").length != 0) { 50 if ($("#parent_search_term").length != 0) {
47 parent_search.initialize_search(); 51 parent_search.initialize_search();
48 } 52 }
49 53
50 if ($("#move_to_search_term").length != 0) { 54 if ($("#move_to_search_term").length != 0) {
51 move_to_search.initialize_search(); 55 move_to_search.initialize_search();
52 } 56 }
53 57
58 if ($("#event_node_search_term").length != 0) {
59 event_search.initialize_search();
60 }
61
62 if ($("#related_asset_search_term").length != 0) {
63 related_assets.initialize();
64 }
65
66 if ($("#rrule_builder").length != 0) {
67 rrule_builder.initialize();
68 }
69
54 if ($('#recent_changes_toggle').length != 0) { 70 if ($('#recent_changes_toggle').length != 0) {
55 hide_all(); 71 hide_all();
56 $('#recent_changes_toggle').attr("class", "selected"); 72 $('#recent_changes_toggle').attr("class", "selected");
57 $('#recent_changes_table').show(); 73 $('#recent_changes_table').show();
58 74
59 $('#recent_changes_toggle').bind("click", function(){ 75 $('#recent_changes_toggle').bind("click", function(){
60 hide_all(); 76 hide_all();
61 $('#recent_changes_toggle').attr("class", "selected"); 77 $('#recent_changes_toggle').attr("class", "selected");
62 $('#recent_changes_table').show(); 78 $('#recent_changes_table').show();
63 return false; 79 return false;
64 }); 80 });
65 81
66 $('#my_work_toggle').bind("click", function(){ 82 $('#my_work_toggle').bind("click", function(){
67 hide_all(); 83 hide_all();
68 $('#my_work_toggle').attr("class", "selected"); 84 $('#my_work_toggle').attr("class", "selected");
@@ -76,7 +92,7 @@ $(document).ready(function () {
76 $('#my_work_table').show(); 92 $('#my_work_table').show();
77 return false; 93 return false;
78 }); 94 });
79 95
80 $('#current_drafts_toggle').bind("click", function(){ 96 $('#current_drafts_toggle').bind("click", function(){
81 hide_all(); 97 hide_all();
82 $('#current_drafts_toggle').attr("class", "selected"); 98 $('#current_drafts_toggle').attr("class", "selected");
@@ -98,91 +114,108 @@ $(document).ready(function () {
98 return false; 114 return false;
99 }); 115 });
100 } 116 }
101 117
102 jQuery.ajaxSetup({ 118 jQuery.ajaxSetup({
103 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");} 119 'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");}
104 }); 120 });
105 121
106 $(document).ajaxSend(function(event, request, settings) { 122 $(document).ajaxSend(function(event, request, settings) {
107 if (typeof(AUTH_TOKEN) == "undefined") return; 123 if (typeof(AUTH_TOKEN) == "undefined") return;
108 // settings.data is a serialized string like "foo=bar&baz=boink" (or null) 124 // settings.data is a serialized string like "foo=bar&baz=boink" (or null)
109 settings.data = settings.data || ""; 125 settings.data = settings.data || "";
110 settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN); 126 settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN);
111 }); 127 });
112 128
113}); 129});
114 130
115 131
116meta_data = { 132meta_data = {
117 initialize : function() { 133 initialize : function() {
118 $("#metadata").hide(); 134 document.getElementById("metadata_details").addEventListener("toggle", function() {
119 135 if (this.open) image_interface.initialize();
120 $("#button").click(function () {
121
122 $("#metadata").slideToggle(1200);
123 image_interface.initialize();
124
125 if ($("#button").attr("class") == "unselected") {
126 $("#button").attr("class", "selected");
127
128 }
129 else {
130 $("#button").attr("class", "unselected");
131 $("#image_browser").hide();
132 $("#image_browser_toggle").attr("class", "unselected");
133 }
134
135 return false;
136 }); 136 });
137 } 137 }
138}; 138};
139 139
140cccms = { 140cccms = {
141 setup_autosave : function() { 141 setup_autosave : function() {
142 142
143 var elements = { 143 var elements = {
144 title : $('#page_title'), 144 title : $('#page_title'),
145 abstract : $('#page_abstract'), 145 abstract : $('#page_abstract'),
146 body : $('#page_body_ifr').contents().find('#tinymce'), 146 body : $('#page_body_ifr').contents().find('#tinymce'),
147 } 147 }
148 148
149 var page = { 149 var page = {
150 cached_title : elements.title.val(), 150 cached_title : elements.title.val(),
151 cached_abstract : elements.abstract.val(), 151 cached_abstract : elements.abstract.val(),
152 cached_body : elements.body.html(), 152 cached_body : elements.body.html(),
153 153
154 title_has_changed : function() { 154 title_has_changed : function() {
155 return (elements.title.val() != this.cached_title) 155 return (elements.title.val() != this.cached_title)
156 }, 156 },
157 157
158 abstract_has_changed : function() { 158 abstract_has_changed : function() {
159 return (elements.abstract.val() != this.cached_abstract) 159 return (elements.abstract.val() != this.cached_abstract)
160 }, 160 },
161 161
162 body_has_changed : function() { 162 body_has_changed : function() {
163 return elements.body.html() != this.cached_body 163 return elements.body.html() != this.cached_body
164 } 164 }
165 } 165 }
166 166
167 jQuery.fn.submitWithAjax = function(options) { 167 jQuery.fn.submitWithAjax = function(options) {
168 if (page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) { 168 if (page.title_has_changed() || page.abstract_has_changed() || page.body_has_changed()) {
169 169
170 tinymce.triggerSave();
170 page.cached_title = elements.title.val(); 171 page.cached_title = elements.title.val();
171 page.cached_abstract = elements.abstract.val(); 172 page.cached_abstract = elements.abstract.val();
172 page.cached_body = elements.body.html(); 173 page.cached_body = elements.body.html();
173 174
174 $("#flash").append("<img src='/images/ajax-loader.gif' alt='' />"); 175 var data = this.serializeArray().filter(function(field) {
175 $.post(this.attr("action"), $(this).serialize(), null, "script"); 176 return field.name !== "_method";
176 177 });
178 data.push({ name: "_method", value: "put" });
179
180 $.ajax({
181 type: "POST",
182 url: this.attr("data-autosave-url"),
183 data: data,
184 error: function(xhr) {
185 if (xhr.status === 423) {
186 clearInterval(cccms.autosave_timer);
187 cccms.report_lock_lost($("#page_editor > form").attr("data-show-url"));
188 }
189 // any other failure: quietly retried on the next tick
190 }
191 });
192
177 } 193 }
178 }; 194 };
179 195
180 setInterval('$("#page_editor > form").submitWithAjax()', 7000); 196 cccms.autosave_timer = setInterval(function() {
197 $("#page_editor > form").submitWithAjax();
198 }, 7000);
199 },
200
201 report_lock_lost : function(show_url) {
202 var $flash = $("#flash");
203 if ($flash.length === 0) {
204 $flash = $("<div>", { id: "flash" }).insertAfter(".admin_content_spacer");
205 }
206
207 var $banner = $("<span>", { "class": "warning" }).text(
208 "This page is now locked by someone else — your changes here can no longer be saved. Copy anything you need, then "
209 );
210 $banner.append($("<a>", { href: show_url }).text("return to the published page"));
211 $banner.append(".");
212
213 $flash.html($banner);
181 } 214 }
182} 215}
183 216
184menu_item_sorter = { 217menu_item_sorter = {
185 218
186 initialize : function() { 219 initialize : function() {
187 $("#menu_item_list").sortable({ 220 $("#menu_item_list").sortable({
188 axis: 'y', 221 axis: 'y',
@@ -205,24 +238,24 @@ menu_item_sorter = {
205 } 238 }
206 }); 239 });
207 }, 240 },
208 241
209 placeholder_helper : function(e,ui) { 242 placeholder_helper : function(e,ui) {
210 $(".ui-state-highlight").html("<td colspan='100%'></td>"); 243 $(".ui-state-highlight").html("<td colspan='100%'></td>");
211 } 244 }
212} 245}
213 246
214image_interface = { 247image_interface = {
215 248
216 initialize : function() { 249 initialize : function() {
217 250
218 $("#image_browser").hide(); 251 $("#image_browser").hide();
219 image_interface.initialize_sortable_image_box(); 252 image_interface.initialize_sortable_image_box();
220 image_interface.connect_browser_and_box(); 253 image_interface.connect_browser_and_box();
221 image_interface.set_droppable_behavior(); 254 image_interface.set_droppable_behavior();
222 image_interface.bind_image_browser_toggle(); 255 image_interface.bind_image_browser_toggle();
223 }, 256 },
224 257
225 258
226 set_droppable_behavior : function() { 259 set_droppable_behavior : function() {
227 $("ul#image_box").droppable({ 260 $("ul#image_box").droppable({
228 out : function(event, ui) { 261 out : function(event, ui) {
@@ -238,7 +271,7 @@ image_interface = {
238 } 271 }
239 }); 272 });
240 }, 273 },
241 274
242 connect_browser_and_box : function() { 275 connect_browser_and_box : function() {
243 $("#image_browser ul li").draggable({ 276 $("#image_browser ul li").draggable({
244 connectToSortable : 'ul#image_box', 277 connectToSortable : 'ul#image_box',
@@ -246,9 +279,9 @@ image_interface = {
246 revert : 'invalid' 279 revert : 'invalid'
247 }); 280 });
248 }, 281 },
249 282
250 initialize_sortable_image_box : function() { 283 initialize_sortable_image_box : function() {
251 284
252 $("ul#image_box").sortable({ 285 $("ul#image_box").sortable({
253 revert : true, 286 revert : true,
254 update : function(event, ui) { 287 update : function(event, ui) {
@@ -263,9 +296,9 @@ image_interface = {
263 } 296 }
264 }); 297 });
265 } 298 }
266 }); 299 });
267 }, 300 },
268 301
269 bind_image_browser_toggle : function() { 302 bind_image_browser_toggle : function() {
270 $("#image_browser_toggle").bind("click", function(){ 303 $("#image_browser_toggle").bind("click", function(){
271 if ($("#image_browser_toggle").attr("class") == "unselected") { 304 if ($("#image_browser_toggle").attr("class") == "unselected") {
@@ -276,10 +309,110 @@ image_interface = {
276 $("#image_browser_toggle").attr("class", "unselected"); 309 $("#image_browser_toggle").attr("class", "unselected");
277 $("#image_browser").hide(); 310 $("#image_browser").hide();
278 } 311 }
279 312
280 return false; 313 return false;
281 }); 314 });
282 } 315 }
283} 316}
284
285 317
318rrule_builder = {
319 initialize : function() {
320 rrule_builder.try_populate_from_rrule($("#event_rrule").val());
321
322 $("input[name='rrule_freq']").bind("change", function() {
323 if ($(this).val() === "weekly") { rrule_builder.show_weekly_options(); }
324 else { rrule_builder.show_monthly_options(); }
325 rrule_builder.sync();
326 });
327 $("#rrule_monthly_ordinal").bind("change", function() {
328 $("#rrule_ordinal_fields").toggle($(this).is(":checked"));
329 rrule_builder.sync();
330 });
331 $("#rrule_exclude_month").bind("change", function() {
332 $("#rrule_excluded_month").toggle($(this).is(":checked"));
333 rrule_builder.sync();
334 });
335 $("#rrule_builder input, #rrule_builder select").bind("change", rrule_builder.sync);
336 },
337
338 show_weekly_options : function() {
339 $("#rrule_weekly_options").show();
340 $("#rrule_monthly_options").hide();
341 },
342 show_monthly_options : function() {
343 $("#rrule_weekly_options").hide();
344 $("#rrule_monthly_options").show();
345 },
346
347 sync : function() {
348 var freq = $("input[name='rrule_freq']:checked").val();
349 var parts = [];
350
351 if (freq === "weekly") {
352 parts.push("FREQ=WEEKLY");
353 if ($("#rrule_biweekly").is(":checked")) parts.push("INTERVAL=2");
354 var days = [];
355 $("input[id^='rrule_byday_']:checked").each(function() {
356 days.push($(this).attr("id").replace("rrule_byday_", ""));
357 });
358 if (days.length > 0) parts.push("BYDAY=" + days.join(","));
359 } else {
360 parts.push("FREQ=MONTHLY");
361 if ($("#rrule_monthly_ordinal").is(":checked")) {
362 parts.push("BYDAY=" + $("#rrule_ordinal").val() + $("#rrule_ordinal_day").val());
363 }
364 }
365
366 if ($("#rrule_exclude_month").is(":checked")) {
367 var excluded = parseInt($("#rrule_excluded_month").val(), 10);
368 var months = [];
369 for (var m = 1; m <= 12; m++) { if (m !== excluded) months.push(m); }
370 parts.push("BYMONTH=" + months.join(","));
371 }
372
373 $("#event_rrule").val(parts.join(";"));
374 },
375
376 try_populate_from_rrule : function(rrule) {
377 if (!rrule) return;
378 var parts = {};
379 rrule.split(";").forEach(function(p) {
380 var kv = p.split("=");
381 parts[kv[0]] = kv[1];
382 });
383 if (parts.COUNT || parts.UNTIL) return;
384
385 if (parts.FREQ === "WEEKLY") {
386 $("input[name='rrule_freq'][value='weekly']").prop("checked", true);
387 rrule_builder.show_weekly_options();
388 if (parts.INTERVAL === "2") $("#rrule_biweekly").prop("checked", true);
389 if (parts.BYDAY) {
390 parts.BYDAY.split(",").forEach(function(code) {
391 $("#rrule_byday_" + code).prop("checked", true);
392 });
393 }
394 } else if (parts.FREQ === "MONTHLY") {
395 $("input[name='rrule_freq'][value='monthly']").prop("checked", true);
396 rrule_builder.show_monthly_options();
397 var match = parts.BYDAY && parts.BYDAY.match(/^(-?\d+)([A-Z]{2})$/);
398 if (match) {
399 $("#rrule_monthly_ordinal").prop("checked", true);
400 $("#rrule_ordinal_fields").show();
401 $("#rrule_ordinal").val(match[1]);
402 $("#rrule_ordinal_day").val(match[2]);
403 }
404 } else {
405 return;
406 }
407
408 if (parts.BYMONTH) {
409 var included = parts.BYMONTH.split(",").map(function(s) { return parseInt(s, 10); });
410 var missing = [];
411 for (var m = 1; m <= 12; m++) { if (included.indexOf(m) === -1) missing.push(m); }
412 if (missing.length === 1) {
413 $("#rrule_exclude_month").prop("checked", true);
414 $("#rrule_excluded_month").val(missing[0]).show();
415 }
416 }
417 }
418};
diff --git a/public/javascripts/admin_search.js b/public/javascripts/admin_search.js
index 9bf878b..7604bb7 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 $('#search_results').slideUp();
40 $('#search_results').empty();
41 }
42 }); 28 });
43 }, 29 },
44 30
@@ -49,75 +35,155 @@ 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 $('#search_results').empty();
56 if (results.length) {
57 $('#search_results').append(
58 "<p class='search_more'>Press Enter to see all results ⏎</p>"
59 );
60 }
61 for (result in results) {
62 $('#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 $('#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;
46 var isActive = options.isActive;
47 var resultsHeaderHtml = options.resultsHeaderHtml;
48 var renderResults = options.renderResults;
49 var loadOnFocus = options.loadOnFocus; // optional, fires an initial search on focus with no term typed yet
50 var requestId = 0;
51 var timeout;
52 var results = $(resultsSelector);
74 53
75 initialize_search : function() { 54 function runSearch(term) {
76 $("#menu_search_term").bind("keyup", function() { 55 var thisRequest = ++requestId;
77 if ($(this).attr("value")) { 56 $.ajax({
78 $.ajax({ 57 type: "GET",
79 type: "GET", 58 url: url,
80 url: ADMIN_MENU_SEARCH_URL, 59 data: "search_term=" + term,
81 data: "search_term=" + $(this).attr("value"), 60 dataType: "json",
82 dataType: "json", 61 success: function(data) {
83 success : function(results) { 62 if (thisRequest !== requestId) return;
84 menu_items.show_results(results); 63 results.empty();
64
65 var found;
66 if (renderResults) {
67 found = renderResults(data, results, resultsHeaderHtml);
68 } else {
69 if (data.length && resultsHeaderHtml) {
70 results.append(resultsHeaderHtml);
85 } 71 }
86 }); 72 found = false;
87 } 73 for (var i = 0; i < data.length; i++) {
88 else { 74 (function(node) {
89 $('#search_results').slideUp(); 75 var link;
90 $('#search_results').empty(); 76 if (onSelect) {
77 link = $(
78 "<p><a href='#'>" + node.title +
79 "<span class='result_path'>" + node.unique_name + "</span></a></p>"
80 );
81 link.find("a").bind("click", function() {
82 onSelect(node);
83 results.slideUp();
84 results.empty();
85 return false;
86 });
87 } else {
88 link = $(
89 "<p><a href='" + node.node_path + "'>" + node.title +
90 "<span class='result_path'>" + node.unique_name + "</span></a></p>"
91 );
92 }
93 results.append(link);
94 found = true;
95 })(data[i]);
96 }
97 }
98
99 if (found) {
100 results.slideDown();
101 } else {
102 results.slideUp();
103 }
104 },
105 error: function(xhr, status, error) {
106 console.log("Ajax error:", status, error, xhr.status, xhr.responseText);
91 } 107 }
92 }); 108 });
93 }, 109 }
94 110
95 show_results : function(results) { 111 $(inputSelector).bind("input", function() {
96 $("#search_results").empty(); 112 if (isActive && !isActive()) return;
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 113
114 var term = $(this).val();
115 clearTimeout(timeout);
101 116
102 // Sometimes I don't get jquery; wrap() didn't work *sigh* 117 if (!term) {
103 // Guess I'll need a book someday or another framework 118 results.slideUp();
104 var wrapper = $("<div></div>"); 119 results.empty();
105 $(wrapper).append(link) 120 return;
121 }
106 122
107 $("#search_results").append(wrapper); 123 timeout = setTimeout(function() { runSearch(term); }, 250);
124 });
108 125
109 } 126 if (loadOnFocus) {
110 }, 127 $(inputSelector).bind("focus", function() {
128 if (isActive && !isActive()) return;
129 if ($(this).val()) return; // the input handler above already covers a real term
130 runSearch("");
131 });
132 }
133}
111 134
112 link_closure : function(node) { 135dashboard_search = {
113 var barf = function(){ 136 initialize : function() {
114 $("#menu_item_node_id").val(node.node_id); 137 initSearchPicker({
115 $("#menu_item_path").val("/" + node.unique_name); 138 inputSelector: "#dashboard_search_term",
116 $("#menu_item_title").val(node.title); 139 resultsSelector: "#dashboard_search_results",
117 return false; 140 url: DASHBOARD_SEARCH_URL,
118 } 141 resultsHeaderHtml: "<p class='search_more'>Press Enter to see all results ⏎</p>",
142 renderResults: function(data, results, resultsHeaderHtml) {
143 var found = false;
144
145 if ((data.tags.length || data.nodes.length) && resultsHeaderHtml) {
146 results.append(resultsHeaderHtml);
147 }
148
149 if (data.tags.length) {
150 results.append("<p class='search_group_label'>Tags</p>");
151 var tag_row = $("<div class='search_tag_row'></div>");
152 data.tags.forEach(function(tag) {
153 tag_row.append("<a class='search_tag_pill' href='" + tag.tag_path + "'>" + tag.name + "</a>");
154 found = true;
155 });
156 results.append(tag_row);
157 }
158
159 if (data.nodes.length) {
160 results.append("<p class='search_group_label'>Pages</p>");
161 data.nodes.forEach(function(node) {
162 results.append(
163 "<p><a href='" + node.node_path + "'>" + node.title +
164 "<span class='result_path'>" + node.unique_name + "</span></a></p>"
165 );
166 found = true;
167 });
168 }
169
170 return found;
171 }
172 });
173 }
174};
119 175
120 return barf; 176menu_items = {
177 initialize_search : function() {
178 initSearchPicker({
179 inputSelector: "#menu_search_term",
180 resultsSelector: "#menu_item_search_results",
181 onSelect: function(node) {
182 $("#menu_item_node_id").val(node.node_id);
183 $("#menu_item_path").val("/" + node.unique_name);
184 $("#menu_item_title").val(node.title);
185 }
186 });
121 } 187 }
122}; 188};
123 189
@@ -125,136 +191,93 @@ parent_search = {
125 initialize_search : function() { 191 initialize_search : function() {
126 parent_search.initialize_radio_buttons(); 192 parent_search.initialize_radio_buttons();
127 193
128 $("#parent_search_term").bind("keyup", function() { 194 initSearchPicker({
129 if ($(this).attr("value")) { 195 inputSelector: "#parent_search_term",
130 $.ajax({ 196 resultsSelector: "#parent_search_results",
131 type: "GET", 197 onSelect: function(node) {
132 url: ADMIN_MENU_SEARCH_URL, 198 $("#parent_search_term").val(node.title);
133 data: "search_term=" + $(this).attr("value"), 199 $("#parent_id").val(node.node_id).attr("data-unique-name", node.unique_name);
134 dataType: "json", 200 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 } 201 }
144 }); 202 });
145 },
146 203
147 show_results : function(results) { 204 $("#title").bind("input", function() {
148 $("#search_results").empty(); 205 parent_search.update_resulting_path();
149 var found = false; 206 });
150 for (result in results) {
151 var link = $(("<a href='#'>"+ results[result].title + "</a>"));
152 $(link).bind("click", parent_search.link_closure(results[result]));
153 207
208 $("#copy_resulting_path").bind("click", function() {
209 var path = $("#resulting_path").text();
210 if (path === "—" || !navigator.clipboard) return;
211 navigator.clipboard.writeText(path);
212 });
213 },
154 214
155 // Sometimes I don't get jquery; wrap() didn't work *sigh* 215 update_resulting_path : function() {
156 // Guess I'll need a book someday or another framework 216 var kind = $("input[name='kind']:checked");
157 var wrapper = $("<div></div>"); 217 var title = $("#title").val();
158 $(wrapper).append(link);
159 218
160 $("#search_results").append(wrapper); 219 if (title === "") {
161 found = true; 220 $("#resulting_path").text("—");
221 return;
162 } 222 }
163 if (found)
164 $('#search_results').slideDown();
165 },
166 223
167 link_closure : function(node) { 224 var prefix = kind.val() === "generic"
168 var barf = function(){ 225 ? ($("#parent_id").attr("data-unique-name") || "")
169 $("#parent_search_term").attr("value", node.title); 226 : (kind.attr("data-path-prefix") || "");
170 $("#parent_id").attr("value", node.node_id);
171 $('#search_results').slideUp();
172 $('#search_results').empty();
173 return false;
174 }
175 227
176 return barf; 228 clearTimeout(parent_search.path_timeout);
229 parent_search.path_timeout = setTimeout(function() {
230 $.get(PARAMETERIZE_PREVIEW_URL, { title: title }, function(slug) {
231 $("#resulting_path").text(window.location.origin + "/" + (prefix ? prefix + "/" : "") + slug);
232 });
233 }, 300);
177 }, 234 },
178 235
179 initialize_radio_buttons : function() { 236 initialize_radio_buttons : function() {
180 $("#kind_top_level").bind("change", function(){ 237 parent_search.sync_parent_field();
181 $("#parent_search_field").hide(); 238 $("input[name='kind']").bind("change", function() {
182 }); 239 parent_search.sync_parent_field();
183 240 parent_search.update_resulting_path();
184 $("#kind_update").bind("change", function(){
185 $("#parent_search_field").hide();
186 }); 241 });
242 },
187 243
188 $("#kind_press_release").bind("change", function(){ 244 sync_parent_field : function() {
245 var kind = $("input[name='kind']:checked").val();
246 if (kind === "generic") {
247 $("#parent_search_field").show();
248 } else {
189 $("#parent_search_field").hide(); 249 $("#parent_search_field").hide();
190 }); 250 }
251 }
252};
191 253
192 $("#kind_generic").bind("change", function(){ 254move_to_search = {
193 $("#parent_search_field").show(); 255 initialize_search : function() {
256 initSearchPicker({
257 inputSelector: "#move_to_search_term",
258 resultsSelector: "#move_to_search_results",
259 onSelect: function(node) {
260 $("#move_to_search_term").val(node.title);
261 $("#node_staged_parent_id").val(node.node_id);
262 }
194 }); 263 });
195
196 } 264 }
197} 265};
198 266
199move_to_search = { 267event_search = {
200 initialize_search : function() { 268 initialize_search : function() {
201 $("#move_to_search_term").bind("keyup", function() { move_to_search.do_search($(this))}); 269 initSearchPicker({
202 $("#move_to_search_term").bind("keydown", function() { move_to_search.do_search($(this))}); 270 inputSelector: "#event_node_search_term",
203 $("#move_to_search_term").bind("keypress", function() { move_to_search.do_search($(this))}); 271 resultsSelector: "#event_search_results",
204 $("#move_to_search_term").bind("paste", function() { move_to_search.do_search($(this))}); 272 onSelect: function(node) {
205 $("#move_to_search_term").bind("cut", function() { move_to_search.do_search($(this))}); 273 $("#event_node_search_term").val(node.title);
206 }, 274 $("#event_node_id").val(node.node_id);
207 275
208 do_search : function(_this) { 276 var title_field = $("#event_title");
209 if (_this.attr("value")) { 277 if (title_field.val() === "") {
210 $.ajax({ 278 $("#event_title_hint").text("Using \"" + node.title + "\" from the associated node.");
211 type: "GET",
212 url: ADMIN_MENU_SEARCH_URL,
213 data: "search_term=" + _this.attr("value"),
214 dataType: "json",
215 success : function(results) {
216 move_to_search.show_results(results);
217 } 279 }
218 }); 280 }
219 } 281 });
220 else {
221 $('#search_results').slideUp();
222 $('#search_results').empty();
223 }
224 },
225
226 show_results : function(results) {
227 $("#search_results").empty();
228 var found = false;
229 for (result in results) {
230 var link = $(("<a href='#'>"+ results[result].title + "</a>"));
231 $(link).bind("click", move_to_search.link_closure(results[result]));
232
233
234 // Sometimes I don't get jquery; wrap() didn't work *sigh*
235 // Guess I'll need a book someday or another framework
236 var wrapper = $("<div></div>");
237 $(wrapper).append(link)
238
239 $("#search_results").append(wrapper);
240 found = true;
241 }
242 if (found)
243 $('#search_results').slideDown();
244 else
245 $('#search_results').slideUp();
246
247 },
248
249 link_closure : function(node) {
250 var barf = function(){
251 $("#move_to_search_term").attr("value", node.title);
252 $("#node_staged_parent_id").attr("value", node.node_id);
253 $('#search_results').slideUp();
254 $('#search_results').empty();
255 return false;
256 }
257
258 return barf;
259 } 282 }
260} 283};
diff --git a/public/javascripts/cacycle_diff.js b/public/javascripts/cacycle_diff.js
deleted file mode 100644
index 24f9d0b..0000000
--- a/public/javascripts/cacycle_diff.js
+++ /dev/null
@@ -1,1112 +0,0 @@
1// <pre><nowiki>
2
3/*
4
5Name: diff.js
6Version: 0.9.5a (April 6, 2008)
7Info: http://en.wikipedia.org/wiki/User:Cacycle/diff
8Code: http://en.wikipedia.org/wiki/User:Cacycle/diff.js
9
10JavaScript diff algorithm by [[en:User:Cacycle]] (http://en.wikipedia.org/wiki/User_talk:Cacycle).
11Outputs html/css-formatted new text with highlighted deletions, inserts, and block moves.
12
13The program uses cross-browser code and should work with all modern browsers. It has been tested with:
14* Mozilla Firefox 1.5.0.1
15* Mozilla SeaMonkey 1.0
16* Opera 8.53
17* Internet Explorer 6.0.2900.2180
18* Internet Explorer 7.0.5730.11
19This program is also compatibel with Greasemonkey
20
21An implementation of the word-based algorithm from:
22
23Communications of the ACM 21(4):264 (1978)
24http://doi.acm.org/10.1145/359460.359467
25
26With the following additional feature:
27
28* Word types have been optimized for MediaWiki source texts
29* Additional post-pass 5 code for resolving islands caused by adding
30 two common words at the end of sequences of common words
31* Additional detection of block borders and color coding of moved blocks and their original position
32* Optional "intelligent" omission of unchanged parts from the output
33
34This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]]
35and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]].
36
37Usage: var htmlText = WDiffString(oldText, newText);
38
39This code has been released into the public domain.
40
41Datastructures:
42
43text: an object that holds all text related datastructures
44 .newWords: consecutive words of the new text (N)
45 .oldWords: consecutive words of the old text (O)
46 .newToOld: array of corresponding word number in old text (NA)
47 .oldToNew: array of corresponding word number in new text (OA)
48 .message: output message for testing purposes
49
50symbol['word']: symbol table for passes 1 - 3, holds words as a hash
51 .newCtr: new word occurences counter (NC)
52 .oldCtr: old word occurences counter (OC)
53 .toNew: table last old word number
54 .toOld: last new word number (OLNA)
55
56block: an object that holds block move information
57 blocks indexed after new text:
58 .newStart: new text word number of start of this block
59 .newLength: element number of this block including non-words
60 .newWords: true word number of this block
61 .newNumber: corresponding block index in old text
62 .newBlock: moved-block-number of a block that has been moved here
63 .newLeft: moved-block-number of a block that has been moved from this border leftwards
64 .newRight: moved-block-number of a block that has been moved from this border rightwards
65 .newLeftIndex: index number of a block that has been moved from this border leftwards
66 .newRightIndex: index number of a block that has been moved from this border rightwards
67 blocks indexed after old text:
68 .oldStart: word number of start of this block
69 .oldToNew: corresponding new text word number of start
70 .oldLength: element number of this block including non-words
71 .oldWords: true word number of this block
72
73*/
74
75
76// css for change indicators
77if (typeof(wDiffStyleDelete) == 'undefined') { window.wDiffStyleDelete = 'font-weight: normal; text-decoration: none; color: #fff; background-color: #990033;'; }
78if (typeof(wDiffStyleInsert) == 'undefined') { window.wDiffStyleInsert = 'font-weight: normal; text-decoration: none; color: #fff; background-color: #009933;'; }
79if (typeof(wDiffStyleMoved) == 'undefined') { window.wDiffStyleMoved = 'font-weight: bold; color: #000; vertical-align: text-bottom; font-size: xx-small; padding: 0; border: solid 1px;'; }
80if (typeof(wDiffStyleBlock) == 'undefined') { window.wDiffStyleBlock = [
81 'color: #000; background-color: #ffff80;',
82 'color: #000; background-color: #c0ffff;',
83 'color: #000; background-color: #ffd0f0;',
84 'color: #000; background-color: #ffe080;',
85 'color: #000; background-color: #aaddff;',
86 'color: #000; background-color: #ddaaff;',
87 'color: #000; background-color: #ffbbbb;',
88 'color: #000; background-color: #d8ffa0;',
89 'color: #000; background-color: #d0d0d0;'
90]; }
91
92// html for change indicators, {number} is replaced by the block number
93// {block} is replaced by the block style, class and html comments are important for shortening the output
94if (typeof(wDiffHtmlMovedRight) == 'undefined') { window.wDiffHtmlMovedRight = '<input class="wDiffHtmlMovedRight" type="button" value="&gt;" style="' + wDiffStyleMoved + ' {block}"><!--wDiffHtmlMovedRight-->'; }
95if (typeof(wDiffHtmlMovedLeft) == 'undefined') { window.wDiffHtmlMovedLeft = '<input class="wDiffHtmlMovedLeft" type="button" value="&lt;" style="' + wDiffStyleMoved + ' {block}"><!--wDiffHtmlMovedLeft-->'; }
96
97if (typeof(wDiffHtmlBlockStart) == 'undefined') { window.wDiffHtmlBlockStart = '<span class="wDiffHtmlBlock" style="{block}">'; }
98if (typeof(wDiffHtmlBlockEnd) == 'undefined') { window.wDiffHtmlBlockEnd = '</span><!--wDiffHtmlBlock-->'; }
99
100if (typeof(wDiffHtmlDeleteStart) == 'undefined') { window.wDiffHtmlDeleteStart = '<span class="wDiffHtmlDelete" style="' + wDiffStyleDelete + '">'; }
101if (typeof(wDiffHtmlDeleteEnd) == 'undefined') { window.wDiffHtmlDeleteEnd = '</span><!--wDiffHtmlDelete-->'; }
102
103if (typeof(wDiffHtmlInsertStart) == 'undefined') { window.wDiffHtmlInsertStart = '<span class="wDiffHtmlInsert" style="' + wDiffStyleInsert + '">'; }
104if (typeof(wDiffHtmlInsertEnd) == 'undefined') { window.wDiffHtmlInsertEnd = '</span><!--wDiffHtmlInsert-->'; }
105
106// minimal number of real words for a moved block (0 for always displaying block move indicators)
107if (typeof(wDiffBlockMinLength) == 'undefined') { window.wDiffBlockMinLength = 3; }
108
109// exclude identical sequence starts and endings from change marking
110if (typeof(wDiffWordDiff) == 'undefined') { window.wDiffWordDiff = true; }
111
112// enable recursive diff to resolve problematic sequences
113if (typeof(wDiffRecursiveDiff) == 'undefined') { window.wDiffRecursiveDiff = true; }
114
115// enable block move display
116if (typeof(wDiffShowBlockMoves) == 'undefined') { window.wDiffShowBlockMoves = true; }
117
118// remove unchanged parts from final output
119
120// characters before diff tag to search for previous heading, paragraph, line break, cut characters
121if (typeof(wDiffHeadingBefore) == 'undefined') { window.wDiffHeadingBefore = 1500; }
122if (typeof(wDiffParagraphBefore) == 'undefined') { window.wDiffParagraphBefore = 1500; }
123if (typeof(wDiffLineBeforeMax) == 'undefined') { window.wDiffLineBeforeMax = 1000; }
124if (typeof(wDiffLineBeforeMin) == 'undefined') { window.wDiffLineBeforeMin = 500; }
125if (typeof(wDiffBlankBeforeMax) == 'undefined') { window.wDiffBlankBeforeMax = 1000; }
126if (typeof(wDiffBlankBeforeMin) == 'undefined') { window.wDiffBlankBeforeMin = 500; }
127if (typeof(wDiffCharsBefore) == 'undefined') { window.wDiffCharsBefore = 500; }
128
129// characters after diff tag to search for next heading, paragraph, line break, or characters
130if (typeof(wDiffHeadingAfter) == 'undefined') { window.wDiffHeadingAfter = 1500; }
131if (typeof(wDiffParagraphAfter) == 'undefined') { window.wDiffParagraphAfter = 1500; }
132if (typeof(wDiffLineAfterMax) == 'undefined') { window.wDiffLineAfterMax = 1000; }
133if (typeof(wDiffLineAfterMin) == 'undefined') { window.wDiffLineAfterMin = 500; }
134if (typeof(wDiffBlankAfterMax) == 'undefined') { window.wDiffBlankAfterMax = 1000; }
135if (typeof(wDiffBlankAfterMin) == 'undefined') { window.wDiffBlankAfterMin = 500; }
136if (typeof(wDiffCharsAfter) == 'undefined') { window.wDiffCharsAfter = 500; }
137
138// maximal fragment distance to join close fragments
139if (typeof(wDiffFragmentJoin) == 'undefined') { window.wDiffFragmentJoin = 1000; }
140if (typeof(wDiffOmittedChars) == 'undefined') { window.wDiffOmittedChars = '…'; }
141if (typeof(wDiffOmittedLines) == 'undefined') { window.wDiffOmittedLines = '<hr style="height: 2px; margin: 1em 10%;">'; }
142if (typeof(wDiffNoChange) == 'undefined') { window.wDiffNoChange = '<hr style="height: 2px; margin: 1em 20%;">'; }
143
144// compatibility fix for old name of main function
145window.StringDiff = window.WDiffString;
146
147
148// WDiffString: main program
149// input: oldText, newText, strings containing the texts
150// returns: html diff
151
152window.WDiffString = function(oldText, newText) {
153
154// IE / Mac fix
155 oldText = oldText.replace(/(\r\n)/g, '\n');
156 newText = newText.replace(/(\r\n)/g, '\n');
157
158 var text = {};
159 text.newWords = [];
160 text.oldWords = [];
161 text.newToOld = [];
162 text.oldToNew = [];
163 text.message = '';
164 var block = {};
165 var outText = '';
166
167// trap trivial changes: no change
168 if (oldText == newText) {
169 outText = newText;
170 outText = WDiffEscape(outText);
171 outText = WDiffHtmlFormat(outText);
172 return(outText);
173 }
174
175// trap trivial changes: old text deleted
176 if ( (oldText == null) || (oldText.length == 0) ) {
177 outText = newText;
178 outText = WDiffEscape(outText);
179 outText = WDiffHtmlFormat(outText);
180 outText = wDiffHtmlInsertStart + outText + wDiffHtmlInsertEnd;
181 return(outText);
182 }
183
184// trap trivial changes: new text deleted
185 if ( (newText == null) || (newText.length == 0) ) {
186 outText = oldText;
187 outText = WDiffEscape(outText);
188 outText = WDiffHtmlFormat(outText);
189 outText = wDiffHtmlDeleteStart + outText + wDiffHtmlDeleteEnd;
190 return(outText);
191 }
192
193// split new and old text into words
194 WDiffSplitText(oldText, newText, text);
195
196// calculate diff information
197 WDiffText(text);
198
199//detect block borders and moved blocks
200 WDiffDetectBlocks(text, block);
201
202// process diff data into formatted html text
203 outText = WDiffToHtml(text, block);
204
205// IE fix
206 outText = outText.replace(/> ( *)</g, '>&nbsp;$1<');
207
208 return(outText);
209}
210
211
212// WDiffSplitText: split new and old text into words
213// input: oldText, newText, strings containing the texts
214// changes: text.newWords and text.oldWords, arrays containing the texts in arrays of words
215
216window.WDiffSplitText = function(oldText, newText, text) {
217
218// convert strange spaces
219 oldText = oldText.replace(/[\t\u000b\u00a0\u2028\u2029]+/g, ' ');
220 newText = newText.replace(/[\t\u000b\u00a0\u2028\u2029]+/g, ' ');
221
222// split old text into words
223
224// / | | | | | | | | | | | | | | /
225 var pattern = /[\w]+|\[\[|\]\]|\{\{|\}\}|\n+| +|&\w+;|'''|''|=+|\{\||\|\}|\|\-|./g;
226 var result;
227 do {
228 result = pattern.exec(oldText);
229 if (result != null) {
230 text.oldWords.push(result[0]);
231 }
232 } while (result != null);
233
234// split new text into words
235 do {
236 result = pattern.exec(newText);
237 if (result != null) {
238 text.newWords.push(result[0]);
239 }
240 } while (result != null);
241
242 return;
243}
244
245
246// WDiffText: calculate diff information
247// input: text.newWords and text.oldWords, arrays containing the texts in arrays of words
248// optionally for recursive calls: newStart, newEnd, oldStart, oldEnd, recursionLevel
249// changes: text.newToOld and text.oldToNew, containing the line numbers in the other version
250
251window.WDiffText = function(text, newStart, newEnd, oldStart, oldEnd, recursionLevel) {
252
253 symbol = new Object();
254 symbol.newCtr = [];
255 symbol.oldCtr = [];
256 symbol.toNew = [];
257 symbol.toOld = [];
258
259// set defaults
260 newStart = newStart || 0;
261 newEnd = newEnd || text.newWords.length;
262 oldStart = oldStart || 0;
263 oldEnd = oldEnd || text.oldWords.length;
264 recursionLevel = recursionLevel || 0;
265
266// limit recursion depth
267 if (recursionLevel > 10) {
268 return;
269 }
270
271// pass 1: parse new text into symbol table s
272
273 var word;
274 for (var i = newStart; i < newEnd; i ++) {
275 word = text.newWords[i];
276
277// add new entry to symbol table
278 if ( symbol[word] == null) {
279 symbol[word] = { newCtr: 0, oldCtr: 0, toNew: null, toOld: null };
280 }
281
282// increment symbol table word counter for new text
283 symbol[word].newCtr ++;
284
285// add last word number in new text
286 symbol[word].toNew = i;
287 }
288
289// pass 2: parse old text into symbol table
290
291 for (var j = oldStart; j < oldEnd; j ++) {
292 word = text.oldWords[j];
293
294// add new entry to symbol table
295 if ( symbol[word] == null) {
296 symbol[word] = { newCtr: 0, oldCtr: 0, toNew: null, toOld: null };
297 }
298
299// increment symbol table word counter for old text
300 symbol[word].oldCtr ++;
301
302// add last word number in old text
303 symbol[word].toOld = j;
304 }
305
306// pass 3: connect unique words
307
308 for (var i in symbol) {
309
310// find words in the symbol table that occur only once in both versions
311 if ( (symbol[i].newCtr == 1) && (symbol[i].oldCtr == 1) ) {
312 var toNew = symbol[i].toNew;
313 var toOld = symbol[i].toOld;
314
315// do not use spaces as unique markers
316 if ( ! /\s/.test( text.newWords[toNew] ) ) {
317
318// connect from new to old and from old to new
319 text.newToOld[toNew] = toOld;
320 text.oldToNew[toOld] = toNew;
321 }
322 }
323 }
324
325// pass 4: connect adjacent identical words downwards
326
327 for (var i = newStart; i < newEnd - 1; i ++) {
328
329// find already connected pairs
330 if (text.newToOld[i] != null) {
331 j = text.newToOld[i];
332
333// check if the following words are not yet connected
334 if ( (text.newToOld[i + 1] == null) && (text.oldToNew[j + 1] == null) ) {
335
336// if the following words are the same connect them
337 if ( text.newWords[i + 1] == text.oldWords[j + 1] ) {
338 text.newToOld[i + 1] = j + 1;
339 text.oldToNew[j + 1] = i + 1;
340 }
341 }
342 }
343 }
344
345// pass 5: connect adjacent identical words upwards
346
347 for (var i = newEnd - 1; i > newStart; i --) {
348
349// find already connected pairs
350 if (text.newToOld[i] != null) {
351 j = text.newToOld[i];
352
353// check if the preceeding words are not yet connected
354 if ( (text.newToOld[i - 1] == null) && (text.oldToNew[j - 1] == null) ) {
355
356// if the preceeding words are the same connect them
357 if ( text.newWords[i - 1] == text.oldWords[j - 1] ) {
358 text.newToOld[i - 1] = j - 1;
359 text.oldToNew[j - 1] = i - 1;
360 }
361 }
362 }
363 }
364
365// recursively diff still unresolved regions downwards
366
367 if (wDiffRecursiveDiff) {
368 i = newStart;
369 j = oldStart;
370 while (i < newEnd) {
371 if (text.newToOld[i - 1] != null) {
372 j = text.newToOld[i - 1] + 1;
373 }
374
375// check for the start of an unresolved sequence
376 if ( (text.newToOld[i] == null) && (text.oldToNew[j] == null) ) {
377
378// determine the ends of the sequences
379 var iStart = i;
380 var iEnd = i;
381 while ( (text.newToOld[iEnd] == null) && (iEnd < newEnd) ) {
382 iEnd ++;
383 }
384 var iLength = iEnd - iStart;
385
386 var jStart = j;
387 var jEnd = j;
388 while ( (text.oldToNew[jEnd] == null) && (jEnd < oldEnd) ) {
389 jEnd ++;
390 }
391 var jLength = jEnd - jStart;
392
393// recursively diff the unresolved sequence
394 if ( (iLength > 0) && (jLength > 0) ) {
395 if ( (iLength > 1) || (jLength > 1) ) {
396 if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
397 WDiffText(text, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
398 }
399 }
400 }
401 i = iEnd;
402 }
403 else {
404 i ++;
405 }
406 }
407 }
408
409// recursively diff still unresolved regions upwards
410
411 if (wDiffRecursiveDiff) {
412 i = newEnd - 1;
413 j = oldEnd - 1;
414 while (i >= newStart) {
415 if (text.newToOld[i + 1] != null) {
416 j = text.newToOld[i + 1] - 1;
417 }
418
419// check for the start of an unresolved sequence
420 if ( (text.newToOld[i] == null) && (text.oldToNew[j] == null) ) {
421
422// determine the ends of the sequences
423 var iStart = i;
424 var iEnd = i + 1;
425 while ( (text.newToOld[iStart - 1] == null) && (iStart >= newStart) ) {
426 iStart --;
427 }
428 var iLength = iEnd - iStart;
429
430 var jStart = j;
431 var jEnd = j + 1;
432 while ( (text.oldToNew[jStart - 1] == null) && (jStart >= oldStart) ) {
433 jStart --;
434 }
435 var jLength = jEnd - jStart;
436
437// recursively diff the unresolved sequence
438 if ( (iLength > 0) && (jLength > 0) ) {
439 if ( (iLength > 1) || (jLength > 1) ) {
440 if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) {
441 WDiffText(text, iStart, iEnd, jStart, jEnd, recursionLevel + 1);
442 }
443 }
444 }
445 i = iStart - 1;
446 }
447 else {
448 i --;
449 }
450 }
451 }
452 return;
453}
454
455
456// WDiffToHtml: process diff data into formatted html text
457// input: text.newWords and text.oldWords, arrays containing the texts in arrays of words
458// text.newToOld and text.oldToNew, containing the line numbers in the other version
459// block data structure
460// returns: outText, a html string
461
462window.WDiffToHtml = function(text, block) {
463
464 var outText = text.message;
465
466 var blockNumber = 0;
467 var i = 0;
468 var j = 0;
469 var movedAsInsertion;
470
471// cycle through the new text
472 do {
473 var movedIndex = [];
474 var movedBlock = [];
475 var movedLeft = [];
476 var blockText = '';
477 var identText = '';
478 var delText = '';
479 var insText = '';
480 var identStart = '';
481
482// check if a block ends here and finish previous block
483 if (movedAsInsertion != null) {
484 if (movedAsInsertion == false) {
485 identStart += wDiffHtmlBlockEnd;
486 }
487 else {
488 identStart += wDiffHtmlInsertEnd;
489 }
490 movedAsInsertion = null;
491 }
492
493// detect block boundary
494 if ( (text.newToOld[i] != j) || (blockNumber == 0 ) ) {
495 if ( ( (text.newToOld[i] != null) || (i >= text.newWords.length) ) && ( (text.oldToNew[j] != null) || (j >= text.oldWords.length) ) ) {
496
497// block moved right
498 var moved = block.newRight[blockNumber];
499 if (moved > 0) {
500 var index = block.newRightIndex[blockNumber];
501 movedIndex.push(index);
502 movedBlock.push(moved);
503 movedLeft.push(false);
504 }
505
506// block moved left
507 moved = block.newLeft[blockNumber];
508 if (moved > 0) {
509 var index = block.newLeftIndex[blockNumber];
510 movedIndex.push(index);
511 movedBlock.push(moved);
512 movedLeft.push(true);
513 }
514
515// check if a block starts here
516 moved = block.newBlock[blockNumber];
517 if (moved > 0) {
518
519// mark block as inserted text
520 if (block.newWords[blockNumber] < wDiffBlockMinLength) {
521 identStart += wDiffHtmlInsertStart;
522 movedAsInsertion = true;
523 }
524
525// mark block by color
526 else {
527 if (moved > wDiffStyleBlock.length) {
528 moved = wDiffStyleBlock.length;
529 }
530 identStart += WDiffHtmlCustomize(wDiffHtmlBlockStart, moved - 1);
531 movedAsInsertion = false;
532 }
533 }
534
535 if (i >= text.newWords.length) {
536 i ++;
537 }
538 else {
539 j = text.newToOld[i];
540 blockNumber ++;
541 }
542 }
543 }
544
545// get the correct order if moved to the left as well as to the right from here
546 if (movedIndex.length == 2) {
547 if (movedIndex[0] > movedIndex[1]) {
548 movedIndex.reverse();
549 movedBlock.reverse();
550 movedLeft.reverse();
551 }
552 }
553
554// handle left and right block moves from this position
555 for (var m = 0; m < movedIndex.length; m ++) {
556
557// insert the block as deleted text
558 if (block.newWords[ movedIndex[m] ] < wDiffBlockMinLength) {
559 var movedStart = block.newStart[ movedIndex[m] ];
560 var movedLength = block.newLength[ movedIndex[m] ];
561 var str = '';
562 for (var n = movedStart; n < movedStart + movedLength; n ++) {
563 str += text.newWords[n];
564 }
565 str = WDiffEscape(str);
566 str = str.replace(/\n/g, '&para;<br>');
567 blockText += wDiffHtmlDeleteStart + str + wDiffHtmlDeleteEnd;
568 }
569
570// add a placeholder / move direction indicator
571 else {
572 if (movedBlock[m] > wDiffStyleBlock.length) {
573 movedBlock[m] = wDiffStyleBlock.length;
574 }
575 if (movedLeft[m]) {
576 blockText += WDiffHtmlCustomize(wDiffHtmlMovedLeft, movedBlock[m] - 1);
577 }
578 else {
579 blockText += WDiffHtmlCustomize(wDiffHtmlMovedRight, movedBlock[m] - 1);
580 }
581 }
582 }
583
584// collect consecutive identical text
585 while ( (i < text.newWords.length) && (j < text.oldWords.length) ) {
586 if ( (text.newToOld[i] == null) || (text.oldToNew[j] == null) ) {
587 break;
588 }
589 if (text.newToOld[i] != j) {
590 break;
591 }
592 identText += text.newWords[i];
593 i ++;
594 j ++;
595 }
596
597// collect consecutive deletions
598 while ( (text.oldToNew[j] == null) && (j < text.oldWords.length) ) {
599 delText += text.oldWords[j];
600 j ++;
601 }
602
603// collect consecutive inserts
604 while ( (text.newToOld[i] == null) && (i < text.newWords.length) ) {
605 insText += text.newWords[i];
606 i ++;
607 }
608
609// remove leading and trailing similarities betweein delText and ins from highlighting
610 var preText = '';
611 var postText = '';
612 if (wDiffWordDiff) {
613 if ( (delText != '') && (insText != '') ) {
614
615// remove leading similarities
616 while ( delText.charAt(0) == insText.charAt(0) && (delText != '') && (insText != '') ) {
617 preText = preText + delText.charAt(0);
618 delText = delText.substr(1);
619 insText = insText.substr(1);
620 }
621
622// remove trailing similarities
623 while ( delText.charAt(delText.length - 1) == insText.charAt(insText.length - 1) && (delText != '') && (insText != '') ) {
624 postText = delText.charAt(delText.length - 1) + postText;
625 delText = delText.substr(0, delText.length - 1);
626 insText = insText.substr(0, insText.length - 1);
627 }
628 }
629 }
630
631// output the identical text, deletions and inserts
632
633// moved from here indicator
634 if (blockText != '') {
635 outText += blockText;
636 }
637
638// identical text
639 if (identText != '') {
640 outText += identStart + WDiffEscape(identText);
641 }
642 outText += preText;
643
644// deleted text
645 if (delText != '') {
646 delText = wDiffHtmlDeleteStart + WDiffEscape(delText) + wDiffHtmlDeleteEnd;
647 delText = delText.replace(/\n/g, '&para;<br>');
648 outText += delText;
649 }
650
651// inserted text
652 if (insText != '') {
653 insText = wDiffHtmlInsertStart + WDiffEscape(insText) + wDiffHtmlInsertEnd;
654 insText = insText.replace(/\n/g, '&para;<br>');
655 outText += insText;
656 }
657 outText += postText;
658 } while (i <= text.newWords.length);
659
660 outText += '\n';
661 outText = WDiffHtmlFormat(outText);
662
663 return(outText);
664}
665
666
667// WDiffEscape: replaces html-sensitive characters in output text with character entities
668
669window.WDiffEscape = function(text) {
670
671 text = text.replace(/&/g, '&amp;');
672 text = text.replace(/</g, '&lt;');
673 text = text.replace(/>/g, '&gt;');
674 text = text.replace(/\"/g, '&quot;');
675
676 return(text);
677}
678
679
680// HtmlCustomize: customize indicator html: replace {number} with the block number, {block} with the block style
681
682window.WDiffHtmlCustomize = function(text, block) {
683
684 text = text.replace(/\{number\}/, block);
685 text = text.replace(/\{block\}/, wDiffStyleBlock[block]);
686
687 return(text);
688}
689
690
691// HtmlFormat: replaces newlines and multiple spaces in text with html code
692
693window.WDiffHtmlFormat = function(text) {
694
695 text = text.replace(/ /g, ' &nbsp;');
696 text = text.replace(/\n/g, '<br>');
697
698 return(text);
699}
700
701
702// WDiffDetectBlocks: detect block borders and moved blocks
703// input: text object, block object
704
705window.WDiffDetectBlocks = function(text, block) {
706
707 block.oldStart = [];
708 block.oldToNew = [];
709 block.oldLength = [];
710 block.oldWords = [];
711 block.newStart = [];
712 block.newLength = [];
713 block.newWords = [];
714 block.newNumber = [];
715 block.newBlock = [];
716 block.newLeft = [];
717 block.newRight = [];
718 block.newLeftIndex = [];
719 block.newRightIndex = [];
720
721 var blockNumber = 0;
722 var wordCounter = 0;
723 var realWordCounter = 0;
724
725// get old text block order
726 if (wDiffShowBlockMoves) {
727 var j = 0;
728 var i = 0;
729 do {
730
731// detect block boundaries on old text
732 if ( (text.oldToNew[j] != i) || (blockNumber == 0 ) ) {
733 if ( ( (text.oldToNew[j] != null) || (j >= text.oldWords.length) ) && ( (text.newToOld[i] != null) || (i >= text.newWords.length) ) ) {
734 if (blockNumber > 0) {
735 block.oldLength[blockNumber - 1] = wordCounter;
736 block.oldWords[blockNumber - 1] = realWordCounter;
737 wordCounter = 0;
738 realWordCounter = 0;
739 }
740
741 if (j >= text.oldWords.length) {
742 j ++;
743 }
744 else {
745 i = text.oldToNew[j];
746 block.oldStart[blockNumber] = j;
747 block.oldToNew[blockNumber] = text.oldToNew[j];
748 blockNumber ++;
749 }
750 }
751 }
752
753// jump over identical pairs
754 while ( (i < text.newWords.length) && (j < text.oldWords.length) ) {
755 if ( (text.newToOld[i] == null) || (text.oldToNew[j] == null) ) {
756 break;
757 }
758 if (text.oldToNew[j] != i) {
759 break;
760 }
761 i ++;
762 j ++;
763 wordCounter ++;
764 if ( /\w/.test( text.newWords[i] ) ) {
765 realWordCounter ++;
766 }
767 }
768
769// jump over consecutive deletions
770 while ( (text.oldToNew[j] == null) && (j < text.oldWords.length) ) {
771 j ++;
772 }
773
774// jump over consecutive inserts
775 while ( (text.newToOld[i] == null) && (i < text.newWords.length) ) {
776 i ++;
777 }
778 } while (j <= text.oldWords.length);
779
780// get the block order in the new text
781 var lastMin;
782 var currMinIndex;
783 lastMin = null;
784
785// sort the data by increasing start numbers into new text block info
786 for (var i = 0; i < blockNumber; i ++) {
787 currMin = null;
788 for (var j = 0; j < blockNumber; j ++) {
789 curr = block.oldToNew[j];
790 if ( (curr > lastMin) || (lastMin == null) ) {
791 if ( (curr < currMin) || (currMin == null) ) {
792 currMin = curr;
793 currMinIndex = j;
794 }
795 }
796 }
797 block.newStart[i] = block.oldToNew[currMinIndex];
798 block.newLength[i] = block.oldLength[currMinIndex];
799 block.newWords[i] = block.oldWords[currMinIndex];
800 block.newNumber[i] = currMinIndex;
801 lastMin = currMin;
802 }
803
804// detect not moved blocks
805 for (var i = 0; i < blockNumber; i ++) {
806 if (block.newBlock[i] == null) {
807 if (block.newNumber[i] == i) {
808 block.newBlock[i] = 0;
809 }
810 }
811 }
812
813// detect switches of neighbouring blocks
814 for (var i = 0; i < blockNumber - 1; i ++) {
815 if ( (block.newBlock[i] == null) && (block.newBlock[i + 1] == null) ) {
816 if (block.newNumber[i] - block.newNumber[i + 1] == 1) {
817 if ( (block.newNumber[i + 1] - block.newNumber[i + 2] != 1) || (i + 2 >= blockNumber) ) {
818
819// the shorter one is declared the moved one
820 if (block.newLength[i] < block.newLength[i + 1]) {
821 block.newBlock[i] = 1;
822 block.newBlock[i + 1] = 0;
823 }
824 else {
825 block.newBlock[i] = 0;
826 block.newBlock[i + 1] = 1;
827 }
828 }
829 }
830 }
831 }
832
833// mark all others as moved and number the moved blocks
834 j = 1;
835 for (var i = 0; i < blockNumber; i ++) {
836 if ( (block.newBlock[i] == null) || (block.newBlock[i] == 1) ) {
837 block.newBlock[i] = j++;
838 }
839 }
840
841// check if a block has been moved from this block border
842 for (var i = 0; i < blockNumber; i ++) {
843 for (var j = 0; j < blockNumber; j ++) {
844
845 if (block.newNumber[j] == i) {
846 if (block.newBlock[j] > 0) {
847
848// block moved right
849 if (block.newNumber[j] < j) {
850 block.newRight[i] = block.newBlock[j];
851 block.newRightIndex[i] = j;
852 }
853
854// block moved left
855 else {
856 block.newLeft[i + 1] = block.newBlock[j];
857 block.newLeftIndex[i + 1] = j;
858 }
859 }
860 }
861 }
862 }
863 }
864 return;
865}
866
867
868// WDiffShortenOutput: remove unchanged parts from final output
869// input: the output of WDiffString
870// returns: the text with removed unchanged passages indicated by (...)
871
872window.WDiffShortenOutput = function(diffText) {
873
874// html <br/> to newlines
875 diffText = diffText.replace(/<br[^>]*>/g, '\n');
876
877// scan for diff html tags
878 var regExpDiff = new RegExp('<\\w+ class=\\"(\\w+)\\"[^>]*>(.|\\n)*?<!--\\1-->', 'g');
879 var tagStart = [];
880 var tagEnd = [];
881 var i = 0;
882 var found;
883 while ( (found = regExpDiff.exec(diffText)) != null ) {
884
885// combine consecutive diff tags
886 if ( (i > 0) && (tagEnd[i - 1] == found.index) ) {
887 tagEnd[i - 1] = found.index + found[0].length;
888 }
889 else {
890 tagStart[i] = found.index;
891 tagEnd[i] = found.index + found[0].length;
892 i ++;
893 }
894 }
895
896// no diff tags detected
897 if (tagStart.length == 0) {
898 return(wDiffNoChange);
899 }
900
901// define regexps
902 var regExpHeading = new RegExp('\\n=+.+?=+ *\\n|\\n\\{\\||\\n\\|\\}', 'g');
903 var regExpParagraph = new RegExp('\\n\\n+', 'g');
904 var regExpLine = new RegExp('\\n+', 'g');
905 var regExpBlank = new RegExp('(<[^>]+>)*\\s+', 'g');
906
907// determine fragment border positions around diff tags
908 var rangeStart = [];
909 var rangeEnd = [];
910 var rangeStartType = [];
911 var rangeEndType = [];
912 for (var i = 0; i < tagStart.length; i ++) {
913 var found;
914
915// find last heading before diff tag
916 var lastPos = tagStart[i] - wDiffHeadingBefore;
917 if (lastPos < 0) {
918 lastPos = 0;
919 }
920 regExpHeading.lastIndex = lastPos;
921 while ( (found = regExpHeading.exec(diffText)) != null ) {
922 if (found.index > tagStart[i]) {
923 break;
924 }
925 rangeStart[i] = found.index;
926 rangeStartType[i] = 'heading';
927 }
928
929// find last paragraph before diff tag
930 if (rangeStart[i] == null) {
931 lastPos = tagStart[i] - wDiffParagraphBefore;
932 if (lastPos < 0) {
933 lastPos = 0;
934 }
935 regExpParagraph.lastIndex = lastPos;
936 while ( (found = regExpParagraph.exec(diffText)) != null ) {
937 if (found.index > tagStart[i]) {
938 break;
939 }
940 rangeStart[i] = found.index;
941 rangeStartType[i] = 'paragraph';
942 }
943 }
944
945// find line break before diff tag
946 if (rangeStart[i] == null) {
947 lastPos = tagStart[i] - wDiffLineBeforeMax;
948 if (lastPos < 0) {
949 lastPos = 0;
950 }
951 regExpLine.lastIndex = lastPos;
952 while ( (found = regExpLine.exec(diffText)) != null ) {
953 if (found.index > tagStart[i] - wDiffLineBeforeMin) {
954 break;
955 }
956 rangeStart[i] = found.index;
957 rangeStartType[i] = 'line';
958 }
959 }
960
961// find blank before diff tag
962 if (rangeStart[i] == null) {
963 lastPos = tagStart[i] - wDiffBlankBeforeMax;
964 if (lastPos < 0) {
965 lastPos = 0;
966 }
967 regExpBlank.lastIndex = lastPos;
968 while ( (found = regExpBlank.exec(diffText)) != null ) {
969 if (found.index > tagStart[i] - wDiffBlankBeforeMin) {
970 break;
971 }
972 rangeStart[i] = found.index;
973 rangeStartType[i] = 'blank';
974 }
975 }
976
977// fixed number of chars before diff tag
978 if (rangeStart[i] == null) {
979 rangeStart[i] = tagStart[i] - wDiffCharsBefore;
980 rangeStartType[i] = 'chars';
981 if (rangeStart[i] < 0) {
982 rangeStart[i] = 0;
983 }
984 }
985
986// find first heading after diff tag
987 regExpHeading.lastIndex = tagEnd[i];
988 if ( (found = regExpHeading.exec(diffText)) != null ) {
989 if (found.index < tagEnd[i] + wDiffHeadingAfter) {
990 rangeEnd[i] = found.index + found[0].length;
991 rangeEndType[i] = 'heading';
992 }
993 }
994
995// find first paragraph after diff tag
996 if (rangeEnd[i] == null) {
997 regExpParagraph.lastIndex = tagEnd[i];
998 if ( (found = regExpParagraph.exec(diffText)) != null ) {
999 if (found.index < tagEnd[i] + wDiffParagraphAfter) {
1000 rangeEnd[i] = found.index;
1001 rangeEndType[i] = 'paragraph';
1002 }
1003 }
1004 }
1005
1006// find first line break after diff tag
1007 if (rangeEnd[i] == null) {
1008 regExpLine.lastIndex = tagEnd[i] + wDiffLineAfterMin;
1009 if ( (found = regExpLine.exec(diffText)) != null ) {
1010 if (found.index < tagEnd[i] + wDiffLineAfterMax) {
1011 rangeEnd[i] = found.index;
1012 rangeEndType[i] = 'break';
1013 }
1014 }
1015 }
1016
1017// find blank after diff tag
1018 if (rangeEnd[i] == null) {
1019 regExpBlank.lastIndex = tagEnd[i] + wDiffBlankAfterMin;
1020 if ( (found = regExpBlank.exec(diffText)) != null ) {
1021 if (found.index < tagEnd[i] + wDiffBlankAfterMax) {
1022 rangeEnd[i] = found.index;
1023 rangeEndType[i] = 'blank';
1024 }
1025 }
1026 }
1027
1028// fixed number of chars after diff tag
1029 if (rangeEnd[i] == null) {
1030 rangeEnd[i] = tagEnd[i] + wDiffCharsAfter;
1031 if (rangeEnd[i] > diffText.length) {
1032 rangeEnd[i] = diffText.length;
1033 rangeEndType[i] = 'chars';
1034 }
1035 }
1036 }
1037
1038// remove overlaps, join close fragments
1039 var fragmentStart = [];
1040 var fragmentEnd = [];
1041 var fragmentStartType = [];
1042 var fragmentEndType = [];
1043 fragmentStart[0] = rangeStart[0];
1044 fragmentEnd[0] = rangeEnd[0];
1045 fragmentStartType[0] = rangeStartType[0];
1046 fragmentEndType[0] = rangeEndType[0];
1047 var j = 1;
1048 for (var i = 1; i < rangeStart.length; i ++) {
1049 if (rangeStart[i] > fragmentEnd[j - 1] + wDiffFragmentJoin) {
1050 fragmentStart[j] = rangeStart[i];
1051 fragmentEnd[j] = rangeEnd[i];
1052 fragmentStartType[j] = rangeStartType[i];
1053 fragmentEndType[j] = rangeEndType[i];
1054 j ++;
1055 }
1056 else {
1057 fragmentEnd[j - 1] = rangeEnd[i];
1058 fragmentEndType[j - 1] = rangeEndType[i];
1059 }
1060 }
1061
1062// assemble the fragments
1063 var outText = '';
1064 for (var i = 0; i < fragmentStart.length; i ++) {
1065
1066// get text fragment
1067 var fragment = diffText.substring(fragmentStart[i], fragmentEnd[i]);
1068 var fragment = fragment.replace(/^\n+|\n+$/g, '');
1069
1070// add inline marks for omitted chars and words
1071 if (fragmentStart[i] > 0) {
1072 if (fragmentStartType[i] == 'chars') {
1073 fragment = wDiffOmittedChars + fragment;
1074 }
1075 else if (fragmentStartType[i] == 'blank') {
1076 fragment = wDiffOmittedChars + ' ' + fragment;
1077 }
1078 }
1079 if (fragmentEnd[i] < diffText.length) {
1080 if (fragmentStartType[i] == 'chars') {
1081 fragment = fragment + wDiffOmittedChars;
1082 }
1083 else if (fragmentStartType[i] == 'blank') {
1084 fragment = fragment + ' ' + wDiffOmittedChars;
1085 }
1086 }
1087
1088// add omitted line separator
1089 if (fragmentStart[i] > 0) {
1090 outText += wDiffOmittedLines;
1091 }
1092
1093// encapsulate span errors
1094 outText += '<div>' + fragment + '</div>';
1095 }
1096
1097// add trailing omitted line separator
1098 if (fragmentEnd[i - 1] < diffText.length) {
1099 outText = outText + wDiffOmittedLines;
1100 }
1101
1102// remove leading and trailing empty lines
1103 outText = outText.replace(/^(<div>)\n+|\n+(<\/div>)$/g, '$1$2');
1104
1105// convert to html linebreaks
1106 outText = outText.replace(/\n/g, '<br />');
1107
1108 return(outText);
1109}
1110
1111
1112// <pre><nowiki> \ No newline at end of file
diff --git a/public/javascripts/related_assets.js b/public/javascripts/related_assets.js
new file mode 100644
index 0000000..2955cbf
--- /dev/null
+++ b/public/javascripts/related_assets.js
@@ -0,0 +1,80 @@
1related_assets = {
2 initialize: function() {
3 var container = $("#related_assets");
4 var searchUrl = container.data("search-url");
5 var createUrl = container.data("create-url");
6 var list = $("#related_asset_list");
7
8 initSearchPicker({
9 inputSelector: "#related_asset_search_term",
10 resultsSelector: "#related_asset_search_results",
11 url: searchUrl,
12 loadOnFocus: true,
13 renderResults: function(data, results) {
14 var found = false;
15 data.forEach(function(asset) {
16 var item = $(
17 "<a href='#' class='related_asset_result'>" +
18 "<img src='" + asset.thumb_url + "' alt=''>" +
19 "<span>" + asset.name + "</span>" +
20 "</a>"
21 );
22 item.bind("click", function() {
23 related_assets.attach(asset.id, createUrl, list);
24 return false;
25 });
26 results.append(item);
27 found = true;
28 });
29 return found;
30 }
31 });
32
33 $(document).on("click", function(e) {
34 if (!$(e.target).closest("#related_assets").length) {
35 $("#related_asset_search_results").slideUp().empty();
36 }
37 });
38
39 list.sortable({
40 handle: ".related_asset_handle",
41 update: function(event, ui) {
42 var newPosition = ui.item.index() + 1; // acts_as_list is 1-based; jQuery UI's index() is 0-based
43 $.ajax({
44 type: "PATCH",
45 url: ui.item.data("url"),
46 data: "position=" + newPosition
47 });
48 }
49 });
50
51 list.on("click", ".related_asset_remove", function() {
52 var item = $(this).closest("li");
53 $.ajax({
54 type: "DELETE",
55 url: item.data("url"),
56 success: function() {
57 item.remove();
58 }
59 });
60 });
61 },
62
63 attach: function(assetId, createUrl, list) {
64 $.ajax({
65 type: "POST",
66 url: createUrl,
67 data: "asset_id=" + assetId,
68 dataType: "json",
69 success: function(related) {
70 var item = $($("#related_asset_template").html().trim());
71 item.attr("data-url", related.url);
72 item.find("img").attr("src", related.thumb_url);
73 list.append(item);
74 list.sortable("refresh");
75 $("#related_asset_search_term").val("");
76 $("#related_asset_search_results").slideUp().empty();
77 }
78 });
79 }
80};
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index c9ef173..f1f4c05 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -1,4 +1,6 @@
1/* Layout */ 1/* ============================================================
2 Base elements
3 ============================================================ */
2 4
3body { 5body {
4 font-family: Helvetica, Arial, sans-serif; 6 font-family: Helvetica, Arial, sans-serif;
@@ -6,9 +8,16 @@ body {
6 margin: 0px; 8 margin: 0px;
7} 9}
8 10
11/* Plain link visibility: a light, desaturated squiggly underline
12 signals "clickable" at rest without the weight of a solid line. */
9a { 13a {
10 color: #000000; 14 color: #000000;
11 text-decoration: none; 15 text-decoration: underline;
16 -webkit-text-decoration-style: wavy;
17 text-decoration-style: wavy;
18 text-decoration-color: #b0b0b0;
19 text-decoration-thickness: 1px;
20 text-underline-offset: 2px;
12} 21}
13 22
14a:hover { 23a:hover {
@@ -19,29 +28,117 @@ th {
19 text-transform: lowercase; 28 text-transform: lowercase;
20} 29}
21 30
31td {
32 padding-top: 10px;
33 padding-bottom: 5px;
34 padding-right: 10px;
35}
36/* table#revisions td only overrides padding-right, so its rows still
37 depend on this rule for vertical padding. table.node_table td resets
38 padding-top/bottom to 0 explicitly and does not depend on it. */
39
40input[type=text], textarea {
41 font-size: 1rem;
42 font-family: Helvetica;
43 border: 1px solid #989898;
44}
45
46select {
47 font-size: 1rem;
48 border: 1px solid #989898;
49}
50
51summary {
52 cursor: pointer;
53}
54
55summary:hover {
56 background-color: #f7f7f7;
57}
58
59input[type=password] {
60 border: 1px solid #989898;
61}
62
63input[type=radio] {
64 border: 1px solid #989898;
65}
66
67/* ============================================================
68 Layout
69 ============================================================ */
70
71@media(min-width:1016px) {
72 #wrapper {
73 margin: 0 125px;
74 }
75 .node_action_bar.node_action_bar_save {
76 margin-left: 120px;
77 }
78
79 .standalone_action_bar {
80 margin-left: -125px;
81 }
82}
83@media(max-width:1015px) {
84 #wrapper {
85 margin: 0;
86 width: 100%;
87 box-sizing: border-box;
88 }
89
90 #sitemap details details {
91 margin-left: 0.75rem;
92 padding-left: 0.5rem;
93 }
94}
95
22#metadata, 96#metadata,
23#content { 97#content {
24 margin-left: 5px; 98 margin-left: 5px;
25} 99}
26 100
27@media(min-width:1016px) { 101#metadata_details summary {
28#wrapper { 102 margin-bottom: 1em;
29 margin: 0 125px;
30} 103}
104
105.right { text-align: right; }
106.clear_left { clear: left; }
107
108/* ============================================================
109 Navigation
110 ============================================================ */
111
112#navigation {
113 position: relative;
114 margin-top: 10px;
31} 115}
32@media(max-width:1015px) { 116
33#wrapper { 117.admin_content_spacer {
34 margin: 0; 118 clear: both;
35 width: 100%; 119 margin-bottom: 40px;
36 box-sizing: border-box;
37} 120}
121
122.node_action_bar {
123 display: flex;
124 flex-wrap: wrap;
125 align-items: center;
126 gap: 0.75rem;
127 margin: 1rem 0 1.5rem;
128 padding-bottom: 1rem;
129 border-bottom: 1px solid #e8e8e8;
38} 130}
39 131
132/* ============================================================
133 Flash / notices
134 ============================================================ */
40 135
41#flash { 136#flash {
42 height: 12px; 137 height: auto;
43 line-height: 20px; 138 line-height: 20px;
44 margin-top: 40px; 139 padding: 6px 10px;
140 border-left: 3px solid #969696;
141 background-color: #f1f1f1;
45 margin-bottom: 20px; 142 margin-bottom: 20px;
46 margin-left: 5px; 143 margin-left: 5px;
47} 144}
@@ -52,7 +149,6 @@ th {
52 text-transform: lowercase; 149 text-transform: lowercase;
53} 150}
54 151
55
56#flash img { 152#flash img {
57 height: 12px; 153 height: 12px;
58 width: 12px; 154 width: 12px;
@@ -68,62 +164,87 @@ span#flash_error, span.warning {
68 padding-bottom: 1px; 164 padding-bottom: 1px;
69} 165}
70 166
71/* Pagination */ 167span.warning a {
168 color: inherit;
169 text-decoration: underline;
170}
72 171
73div.pagination { 172.error_messages {
74 padding-top: 5px; 173 border-left: 3px solid #cc0000;
75 padding-bottom: 15px; 174 background-color: #fdecea;
175 padding: 6px 10px;
176 margin-bottom: 1rem;
76} 177}
77 178
78div.pagination span.current, div.pagination a:hover { 179.error_messages ul {
79 color: #ff9600; 180 margin: 0;
181 padding-left: 1.25rem;
80} 182}
81 183
82/* Styles */ 184/* ============================================================
185 Pagination
186 ============================================================ */
83 187
84.right { 188div.pagination {
85 text-align: right; 189 padding-top: 5px;
190 padding-bottom: 15px;
191 display: flex;
192 align-items: center;
193 gap: 0.35rem;
86} 194}
87 195
88.clear_left { 196div.pagination a,
89 clear: left; 197div.pagination em.current,
198div.pagination span.gap {
199 display: inline-flex;
200 align-items: center;
201 justify-content: center;
202 min-width: 1.75rem;
203 height: 1.75rem;
204 padding: 0 0.4rem;
205 border-radius: 4px;
206 font-style: normal;
90} 207}
91 208
92#navigation { 209div.pagination a:hover {
93 position: relative; 210 color: #ff9600;
94 margin-top: 10px;
95} 211}
96 212
97#navigation div { 213div.pagination em.current {
98 float: left; 214 background-color: #ff9600;
215 color: #ffffff;
216 font-weight: bold;
99} 217}
100 218
101#navigation a:hover { 219div.pagination span.gap {
102 color: #ffffff; 220 color: #969696;
103 background-color: #000000;
104} 221}
105 222
106#main_navigation a, 223div.pagination .previous_page.disabled,
107#sub_navigation a, 224div.pagination .next_page.disabled {
108#metadata a, 225 color: #969696;
109input[type=submit], 226 cursor: not-allowed;
110#overview_toggle a {
111 letter-spacing: 1px;
112 padding-left: 5px;
113 padding-right: 5px;
114 padding-top: 1px;
115 padding-bottom: 1px;
116 text-transform: lowercase;
117} 227}
118 228
119/* button_to forms: render submit inputs as plain links */ 229/* ============================================================
230 Buttons
231 ============================================================ */
232
233/* button_to forms: render submit inputs/buttons as plain links,
234 orange on hover; .destructive variant is red. */
120form.button_to { 235form.button_to {
121 display: inline; 236 display: inline;
122 margin: 0; 237 margin: 0;
123 padding: 0; 238 padding: 0;
124} 239}
125 240
126form.button_to input[type="submit"] { 241form.button_to input[type="submit"],
242form.button_to button[type="submit"] {
243 display: inline-flex;
244 align-items: center;
245 gap: 0.5rem;
246 -webkit-appearance: none;
247 appearance: none;
127 background: none; 248 background: none;
128 border: none; 249 border: none;
129 padding: 0; 250 padding: 0;
@@ -131,80 +252,231 @@ form.button_to input[type="submit"] {
131 font: inherit; 252 font: inherit;
132 color: inherit; 253 color: inherit;
133 cursor: pointer; 254 cursor: pointer;
134 text-decoration: none 255 text-decoration: none;
135} 256}
136 257
137form.button_to input[type="submit"]:hover { 258form.button_to.state_changing input[type="submit"],
259form.button_to.state_changing button[type="submit"] {
260 color: #0d47a1;
261 background-color: #e3f2fd;
262}
263
264form.button_to.state_changing input[type="submit"]:hover,
265form.button_to.state_changing button[type="submit"]:hover {
138 color: #ffffff; 266 color: #ffffff;
139 background-color: #ff9600; 267 background-color: #1565c0;
140} 268}
141 269
142#admin_wizard { 270form.button_to.destructive input[type="submit"],
143 margin-bottom: 2rem; 271form.button_to.destructive button[type="submit"] {
272 color: #cc0000;
273 background-color: #fdecea;
144} 274}
145 275
146#page_submit a, 276form.button_to.destructive input[type="submit"]:hover,
147.admin_wizard_button a { 277form.button_to.destructive button[type="submit"]:hover {
148 color: white !important; 278 color: #ffffff;
279 background-color: #cc0000;
149} 280}
150 281
151#page_submit, 282form.button_to.computation input[type="submit"],
152.admin_wizard_button { 283form.button_to.computation button[type="submit"] {
153 display: inline-block; 284 color: #00838f;
154 font-size: 1rem; 285 background-color: #e0f2f1;
155 font-weight: bold;
156 padding: 0.5rem;
157 margin-bottom: 0.5rem;
158 background-color: green;
159} 286}
160 287
161#page_submit:hover, 288form.button_to.computation input[type="submit"]:hover,
162.admin_wizard_button:hover { 289form.button_to.computation button[type="submit"]:hover {
163 background-color: lime !important; 290 color: #ffffff;
291 background-color: #00838f;
164} 292}
165 293
166#sub_navigation a { 294form.button_to.computation input[type="submit"]:disabled,
167 color: #969696; 295form.button_to.computation button[type="submit"]:disabled {
296 opacity: 0.4;
297 cursor: not-allowed;
298 pointer-events: none;
168} 299}
169 300
170#sub_navigation a:hover, #metadata a:hover { 301/* Native (non-button_to) submit buttons -- Create/Save/Publish forms.
171 color: #ffffff; 302 Lower specificity than form.button_to's own rule, so button_to forms
172 background-color: #ff9600; 303 are correctly unaffected. If a button_to output ever loses its class
304 wrapper, it will silently fall through to this bordered style instead
305 of rendering as a plain link -- worth knowing, not necessarily fixing. */
306input[type="submit"] {
307 -webkit-appearance: none;
308 appearance: none;
309 background: none;
310 border: 1px solid #000000;
311 border-radius: 2px;
312 padding: 4px 12px;
313 font: inherit;
314 font-weight: bold;
315 color: inherit;
316 cursor: pointer;
317 text-decoration: none;
173} 318}
174 319
175#sub_navigation a.selected { 320input[type="submit"]:hover {
176 color: #ffffff; 321 color: #ffffff;
177 background-color: #ff9600; 322 background-color: #000000;
178} 323}
179 324
180#navigation a.selected, #metadata a.selected, #overview_toggle a.selected { 325/* ============================================================
181 color: #ffffff; 326 Admin dashboard
327 ============================================================ */
328
329#main_navigation {
330 display: flex;
331 align-items: center;
332 gap: 0.5rem;
333}
334
335#main_navigation a,
336#main_navigation form.button_to button {
337 display: inline-flex;
338 text-decoration: none;
339 align-items: center;
340 background-color: #f7f7f7;
341 justify-content: center;
342 width: 2rem;
343 height: 2rem;
344 padding: 0;
345 border-radius: 2px;
346}
347
348#main_navigation a:hover,
349#main_navigation form.button_to button:hover {
182 background-color: #000000; 350 background-color: #000000;
351 color: #ffffff;
352}
353
354#main_navigation svg,
355.action_button svg,
356form.button_to svg {
357 width: 1.25rem;
358 height: 1.25rem;
359}
360
361#dashboard_search {
362 margin-bottom: 1.5rem;
363}
364
365#dashboard_search input[type=text] {
366 width: 100%;
367 box-sizing: border-box;
368 padding: 10px 14px;
369 font-size: 1.1rem;
370 border-radius: 2px;
371}
372
373.button_row {
374 display: flex;
375 flex-wrap: wrap;
376 gap: 0.75rem;
377 align-items: center;
378}
379
380.button_row .action_button,
381.node_search_form .action_button {
382 margin-bottom: 0;
383}
384
385#dashboard_signposts {
386 margin-bottom: 2rem;
387}
388
389#dashboard_housekeeping h3 {
390 margin-bottom: 0.75rem;
391}
392
393#dashboard_widgets {
394 display: flex;
395 flex-wrap: wrap;
396 gap: 1.5rem;
397 margin-bottom: 2rem;
398}
399
400.dashboard_widget {
401 flex: 1 1 300px;
402 border: 1px solid #e8e8e8;
403 border-radius: 6px;
404 padding: 0.5rem 0.75rem;
405}
406
407.dashboard_widget h3 {
408 margin-top: 0;
183} 409}
184 410
185/* Nodes */ 411.dashboard_widget ul {
412 list-style: none;
413 margin: 0 0 0.75rem;
414 padding: 0;
415}
416
417.dashboard_widget li {
418 display: flex;
419 justify-content: space-between;
420 align-items: flex-start;
421 gap: 1rem;
422 padding: 0.35rem 0;
423 border-bottom: 1px solid #f1f1f1;
424}
186 425
187table.node_table { 426.dashboard_widget_meta {
427 white-space: nowrap;
428 color: #969696;
429 font-size: 0.85rem;
430}
431
432/* ============================================================
433 Tables
434 ============================================================ */
435
436/* Generic fallback header row. Shadowed by table.node_table's own
437 tr.header (2rem) and duplicated (same values) by table#revisions's
438 own tr.header below. Applies as-is to #menu_item_list,
439 div#draft_list table, and table.user_table, none of which define
440 their own tr.header. */
441table tr.header {
442 height: 20px;
443 text-align: left;
444}
445
446table.node_table ,
447table.assets_table,
448table.user_table,
449table.events_table {
188 border-collapse: collapse; 450 border-collapse: collapse;
189} 451}
190 452
191table.node_table tr { 453table.node_table tr {
192 min-height: 2rem;
193 border-bottom: 1px solid #000000; 454 border-bottom: 1px solid #000000;
194} 455}
195 456
457table.assets_table tr,
458table.user_table tr,
459table.events_table tr {
460 border-bottom: 1px solid #e8e8e8;
461}
462
196table.node_table th.node_id, 463table.node_table th.node_id,
197table.node_table th.revision, 464table.node_table th.revision {
198table.node_table th.title {
199 min-width: 4rem; 465 min-width: 4rem;
200} 466}
201 467
468/* th.title's min-width is set to 4rem above and 12rem here -- the
469 later, equally-specific rule wins, so th.title ends up at 12rem and
470 the 4rem entry for it above is dead. Likely a copy/paste leftover. */
202table.node_table th.editor, 471table.node_table th.editor,
203table.node_table th.title { 472table.node_table th.title {
204 min-width: 12rem; 473 min-width: 12rem;
205} 474}
206 475
207table.node_table tr.header { 476table.node_table tr.header,
477table.assets_table tr.header,
478table.user_table tr.header,
479table.events_table tr.header {
208 height: 2rem; 480 height: 2rem;
209 text-align: left; 481 text-align: left;
210} 482}
@@ -217,193 +489,389 @@ table.node_table td {
217 min-width: 2rem; 489 min-width: 2rem;
218} 490}
219 491
492table.assets_table td,
493table.user_table td,
494table.events_table td {
495 padding-top: 8px;
496 padding-bottom: 8px;
497 padding-right: 25px;
498 padding-left: 0px;
499 min-width: 2rem;
500}
501
220table.node_table .node_id { 502table.node_table .node_id {
221 padding-left: 10px; 503 padding-left: 10px;
222 padding-right: 15px; 504 padding-right: 15px;
223} 505}
224 506
225table.node_table tr.header:hover { 507table.node_table .actions {
226 background-color: #ffffff; 508 text-transform: lowercase;
227} 509}
228 510
229table.node_table tr:hover { 511table.node_table tr.header:hover,
230 background-color: #f1f1f1; 512table.assets_table tr.header:hover,
513table.user_table tr.header:hover,
514table.events_table tr.header:hover {
515 background-color: #ffffff;
231} 516}
232 517
233table.node_table .actions { 518table.node_table tr:hover,
234 text-transform: lowercase; 519table.assets_table tr:hover,
520table.user_table tr:hover,
521table.events_table tr:hover {
522 background-color: #f1f1f1;
235} 523}
236 524
237#admin_sitemap_table .node_id:before { 525#admin_sitemap_table .node_id:before {
238 content: "• "; 526 content: "• ";
239} 527}
240 528
241/* Revisions */ 529table.revisions_table {
242
243table#revisions {
244 border-collapse: collapse; 530 border-collapse: collapse;
245} 531}
246 532
247table#revisions td { 533table.revisions_table thead {
248 padding-right: 25px; 534 position: sticky;
535 top: 0;
536 z-index: 1;
249} 537}
250 538
251table#revisions td.title { 539table.revisions_table th,
540table.revisions_table td {
541 padding: 0.4rem 0.75rem;
542 text-align: left;
543}
544
545table.revisions_table thead tr.header,
546table.revisions_table thead tr.diff_sticky_bar {
547 background-color: #ffffff;
548}
549
550table.revisions_table .diff_sticky_bar td {
551 padding-top: 0.5rem;
552 padding-bottom: 0.5rem;
553 border-bottom: 1px solid #e8e8e8;
554}
555
556#diff_selection_label {
557 margin-left: 0.75rem;
558 color: #969696;
559}
560table.revisions_table td.title {
252 width: 450px; 561 width: 450px;
253} 562}
254 563
255table#revisions td.date { 564table.revisions_table td.date {
256 width: 175px; 565 width: 175px;
257} 566}
258 567
259table#revisions tr.header:hover { 568table.revisions_table tr:not(.header):not(.no_hover) {
569 border-bottom: 1px solid #e8e8e8;
570}
571
572table.revisions_table tr.header:hover {
260 background-color: #ffffff; 573 background-color: #ffffff;
261} 574}
262 575
263table#revisions tr.no_hover:hover { 576table.revisions_table tr.no_hover:hover {
264 background-color: #ffffff; 577 background-color: #ffffff;
265} 578}
266 579
267table#revisions tr:hover { 580table.revisions_table tr:hover {
268 background-color: #f1f1f1; 581 background-color: #f1f1f1;
269} 582}
270 583
271table#revisions tr.header { 584.events_table .rrule_text {
272 height: 20px; 585 display: inline-block;
273 text-align: left; 586 max-width: 300px;
587 overflow-wrap: break-word;
274} 588}
275 589
276table#revisions tr { 590.events_table .truncate {
277 height: 45px 591 display: inline-block;
592 max-width: 200px;
593 overflow: hidden;
594 text-overflow: ellipsis;
595 white-space: nowrap;
596 vertical-align: bottom;
278} 597}
279 598
280/* Page Editor */ 599#diffview del {
600 background: #ffd7d5;
601 color: #82071e;
602 text-decoration: line-through;
603 padding: 0 2px;
604 border-radius: 2px;
605}
281 606
282input[type=text], textarea { 607#diffview ins {
283 font-size: 1rem; 608 background: #ccffd8;
284 font-family: Helvetica; 609 color: #055d20;
285 border: 1px solid #989898; 610 text-decoration: none;
611 padding: 0 2px;
612 border-radius: 2px;
286} 613}
287 614
288select { 615#diffview .diff_side_by_side {
289 font-size: 1rem; 616 display: flex;
290 border: 1px solid #989898; 617 gap: 1rem;
291} 618}
292 619
293input[type=text]#tag_list, 620#diffview .diff_column {
294input[type=text]#node_staged_slug, 621 flex: 1;
295input#move_to_search_term {
296 padding: 5px;
297} 622}
298 623
299input[type=password] { 624#diffview del.diff_structural,
300 border: 1px solid #989898; 625#diffview ins.diff_structural {
626 cursor: help;
301} 627}
302 628
303input[type=submit] { 629.diff_unchanged {
304 border: none; 630 color: #969696;
305 padding-top: 0px; 631 font-style: italic;
306 padding-bottom: 1px;
307 background-color: #ffffff;
308} 632}
309 633
310input[type=submit]:hover { 634.diff_set_list {
311 border: none; 635 list-style: none;
312 color: #ffffff; 636 padding-left: 0;
313 background-color: #ff9600; 637 margin: 0;
314} 638}
315 639
316input[type=radio] { 640/* ============================================================
317 border: 1px solid #989898; 641 Translation compare view (page_translations#show)
642 ============================================================ */
643
644.translation_compare {
645 display: flex;
646 flex-direction: column;
647 gap: 1.5rem;
318} 648}
319 649
320div#login_form input[type=text], div#login_form input[type=password] { 650@media(min-width:1016px) {
321 width: 150px; 651 .translation_compare {
652 flex-direction: row;
653 gap: 2rem;
654 }
655
656 .translation_compare_column {
657 flex: 1;
658 min-width: 0;
659 }
322} 660}
323 661
662table.user_table td.user_login {
663 padding-right: 30px;
664}
665
666/* ============================================================
667 Node display (nodes#show / nodes#new)
668 ============================================================ */
324 669
325@media(min-width:1016px) { 670@media(min-width:1016px) {
326input#tag_list, 671 div.node_description {
327input#node_staged_slug, 672 float: left;
328input#move_to_search_term { 673 width: 110px;
329 width: 690px; 674 min-height: 2rem;
675 text-align: right;
676 text-transform: lowercase;
677 vertical-align: top;
678 }
679
680 div.node_content {
681 margin-left: 120px;
682 min-height: 2rem;
683 display: block;
684 margin-bottom: 1rem;
685 }
330} 686}
687@media(max-width:1015px) {
688 div.node_description {
689 min-height: 2rem;
690 width: 30em;
691 text-transform: lowercase;
692 vertical-align: top;
693 }
331 694
332input[type=text]#page_title { 695 div.node_content {
333 width: 690px; 696 min-height: 2rem;
334 font-size: 1rem; 697 margin-bottom: 1rem;
335 padding-top: 6px; 698 }
336 padding-bottom: 4px; 699}
337 padding-left: 5px; 700
338 padding-right: 5px; 701.node_content.node_info_group {
702 border: 1px solid #e8e8e8;
703 border-radius: 6px;
704 padding: 0.5rem 0.75rem;
705}
706
707.node_info_group .disabled_action {
708 display: inline-block;
709 border: 1px solid #c0c0c0;
710 border-radius: 2px;
711 padding: 4px 12px;
339 font-weight: bold; 712 font-weight: bold;
713 color: #969696;
714 cursor: not-allowed;
340} 715}
341 716
342textarea#page_abstract { 717.node_info_group_items {
343 width: 690px; 718 display: flex;
344 height: 150px; 719 flex-wrap: wrap;
345 padding: 5px; 720 gap: 0.5rem 2rem;
346} 721}
347 722
348#page_editor textarea#page_body { 723.node_info_item {
349 width: 700px; 724 min-width: 140px;
350 height: 600px;
351} 725}
352 726
353#page_editor #metadata, #page_editor #content, #admin_overview { 727.node_info_label {
354 margin-left: -125px; 728 display: block;
729 font-weight: normal;
730 color: #969696;
731 font-size: 0.85rem;
732 text-transform: lowercase;
355} 733}
356 734
735.node_info_group ul {
736 margin: 0;
737 padding-left: 1.25rem;
357} 738}
358 739
359/* Mobile */ 740.node_info_group li {
360@media(max-width:1015px) { 741 margin-bottom: 0.25rem;
361input#tag_list,
362input#node_staged_slug,
363input#move_to_search_term {
364 box-sizing: border-box;
365 width: 100%;
366} 742}
367 743
368#page_editor #content, 744.node_content.node_status {
369#page_editor #metadata { 745 border: 2px solid #000000;
370 padding-right: 5px; 746 background-color: #fafafa;
371} 747}
372 748
373#page_editor textarea#page_body { 749.node_status .node_info_group_items:first-child {
750 margin-bottom: 0.75rem;
751}
752
753.node_status .node_info_group_items + .node_info_group_items {
754 margin-top: 0.5rem;
755 margin-bottom: 0.5rem;
756}
757
758.node_status .field_hint {
759 margin-top: 0.5rem;
760 display: block;
761}
762
763.action_button,
764form.button_to input[type="submit"],
765form.button_to button[type="submit"] {
766 padding: 4px 12px;
767 line-height: 1.2;
374 box-sizing: border-box; 768 box-sizing: border-box;
375 width: 800px;
376 height: 50rem;
377} 769}
378 770
379#page_editor textarea#page_abstract { 771form.button_to input[type="submit"],
772form.button_to button[type="submit"] {
773 border: 1px solid transparent;
774 border-radius: 2px;
775}
776
777.field_hint {
778 display: block;
779 margin-top: 2px;
780 font-size: 0.75rem;
781 color: #969696;
782 padding-bottom: 4px;
783}
784
785.action_button + .field_hint {
786 display: inline;
787 margin-left: 0.5rem;
788 margin-top: 0;
789}
790
791.revisions_full_history_link {
792 margin-bottom: 0;
793}
794
795.node_search_form {
796 display: flex;
797 align-items: center;
798 gap: 0.5rem;
799 margin-bottom: 0.75rem;
800}
801
802.node_search_form input[type=text] {
803 padding: 4px 12px;
380 box-sizing: border-box; 804 box-sizing: border-box;
381 height: 8rem; 805 height: 2.25rem;
382 width: 100%; 806 width: 22rem;
807 border-radius: 2px;
383} 808}
384 809
385input[type=text]#page_title { 810.node_search_form .action_button {
811 height: 2.25rem;
386 box-sizing: border-box; 812 box-sizing: border-box;
387 width: 100%;
388 font-size: 2rem;
389 padding-top: 6px;
390 padding-bottom: 4px;
391 padding-left: 5px;
392 font-weight: bold;
393} 813}
394 814
395input[type=text], textarea { 815/* Layout only -- the at-rest visibility (wavy underline) for these
396 font-size: 1.5rem; 816 links comes from the scoped rule in Base elements above. */
817.add_child_links {
818 margin-top: 0.5rem;
397} 819}
398select { 820
399 font-size: 1.5rem; 821.add_child_links a {
822 white-space: nowrap;
400} 823}
401 824
402#metadata ul#image_box { 825.sitemap_node {
403 width: 100% !important; 826 padding-bottom: 0.5rem;
827 margin-bottom: 0.5rem;
828 border-bottom: 1px solid #ececec;
404} 829}
830
831.sitemap_node h4 {
832 margin: 0;
833}
834
835.sitemap_node .field_hint {
836 display: block;
837 margin: 2px 0 0;
405} 838}
406 839
840.sitemap_node p {
841 margin: 2px 0 0;
842}
843
844#sitemap details details {
845 margin-left: 1.5rem;
846 border-left: 1px solid #e8e8e8;
847 padding-left: 0.75rem;
848}
849
850#sitemap summary {
851 padding: 4px 0;
852}
853
854#sitemap details:not([open]) > summary {
855 color: #ff9600;
856 font-weight: bold;
857}
858
859.aligned_action_row {
860 display: flex;
861 flex-wrap: wrap;
862 align-items: start;
863 gap: 0.5rem;
864}
865
866.aligned_action_row .action_button,
867.aligned_action_row form.button_to {
868 margin-top: -5px;
869}
870
871/* ============================================================
872 Page editor / metadata forms
873 ============================================================ */
874
407#new_node { 875#new_node {
408 margin-left: -118px; 876 margin-left: -118px;
409} 877}
@@ -412,51 +880,158 @@ select {
412 width: 350px; 880 width: 350px;
413} 881}
414 882
415tr {} 883div#login_form input[type=text], div#login_form input[type=password] {
884 width: 150px;
885}
416 886
417td { 887input[type=text]#tag_list,
418 padding-top: 10px; 888input[type=text]#node_staged_slug,
419 padding-bottom: 5px; 889input#move_to_search_term {
420 padding-right: 10px; 890 padding: 5px;
421} 891}
422 892
423@media(min-width:1016px) { 893div#page_editor {
424div.node_description { 894 margin-top: 0px;
425 float: left; 895 margin-left: 10px;
426 width: 110px;
427 min-height: 2rem;
428 text-align: right;
429 text-transform: lowercase;
430 vertical-align: top;
431} 896}
432 897
433div.node_content { 898#page_editor a.action_button,
434 margin-left: 120px; 899.action_button {
435 min-height: 2rem; 900 display: inline-flex;
436 display: block; 901 align-items: center;
902 gap: 0.5rem;
903 -webkit-appearance: none;
904 appearance: none;
905 border: 1px solid #000000;
906 border-radius: 2px;
907 padding: 4px 12px;
908 font-weight: bold;
909 text-decoration: none;
910 color: #000000;
437 margin-bottom: 1rem; 911 margin-bottom: 1rem;
912 background: none;
913 cursor: pointer;
914 font: inherit;
438} 915}
439} 916
440@media(max-width:1015px) { 917#page_editor a.action_button:hover,
441div.node_description { 918.action_button:hover {
442 min-height: 2rem; 919 color: #ffffff;
443 width: 30em; 920 background-color: #000000;
444 text-transform: lowercase;
445 vertical-align: top;
446} 921}
447 922
448div.node_content { 923#page_editor input[type=text],
449 min-height: 2rem; 924#page_editor input[type=password],
450 margin-bottom: 1rem; 925#page_editor textarea {
926 padding: 5px;
451} 927}
452 928
929@media(min-width:1016px) {
930 input#tag_list,
931 input#menu_search_term,
932 input#menu_item_node_id,
933 input#menu_item_path,
934 input#menu_item_title,
935 input#node_staged_slug,
936 input#move_to_search_term {
937 width: 690px;
938 }
939
940 input[type=text]#page_title {
941 width: 690px;
942 font-size: 1rem;
943 padding-top: 6px;
944 padding-bottom: 4px;
945 padding-left: 5px;
946 padding-right: 5px;
947 font-weight: bold;
948 }
949
950 textarea#page_abstract {
951 width: 690px;
952 height: 150px;
953 padding: 5px;
954 }
955
956 #page_editor textarea#page_body {
957 width: 700px;
958 height: 600px;
959 }
960
961 #page_editor #metadata, #page_editor #content, #admin_overview {
962 margin-left: -125px;
963 }
964
965 #page_editor input[type=text],
966 #page_editor input[type=password],
967 #page_editor textarea {
968 width: 690px;
969 }
453} 970}
454 971
455div#page_editor { 972@media(max-width:1015px) {
456 margin-top: 0px; 973 input#tag_list,
457 margin-left: 10px; 974 input#node_staged_slug,
975 input#move_to_search_term {
976 box-sizing: border-box;
977 width: 100%;
978 }
979
980 #page_editor #content,
981 #page_editor #metadata {
982 padding-right: 5px;
983 }
984
985 /* Fixed 800px width will overflow narrow viewports despite
986 border-box -- every sibling rule in this block uses 100% instead.
987 Worth checking on an actual phone; looks unfinished rather than
988 intentional. Not changed here since it wasn't part of what you
989 asked to fix. */
990 #page_editor textarea#page_body {
991 box-sizing: border-box;
992 width: 800px;
993 height: 50rem;
994 }
995
996 #page_editor textarea#page_abstract {
997 box-sizing: border-box;
998 height: 8rem;
999 width: 100%;
1000 }
1001
1002 input[type=text]#page_title {
1003 box-sizing: border-box;
1004 width: 100%;
1005 font-size: 2rem;
1006 padding-top: 6px;
1007 padding-bottom: 4px;
1008 padding-left: 5px;
1009 font-weight: bold;
1010 }
1011
1012 #page_editor input[type=text],
1013 #page_editor input[type=password],
1014 #page_editor textarea {
1015 box-sizing: border-box;
1016 width: 100%;
1017 }
1018
1019 input[type=text], textarea {
1020 font-size: 1.5rem;
1021 }
1022 select {
1023 font-size: 1.5rem;
1024 }
1025
1026 #metadata ul#image_box {
1027 width: 100% !important;
1028 }
458} 1029}
459 1030
1031/* ============================================================
1032 Draft list (dashboard widget)
1033 ============================================================ */
1034
460div#draft_list { 1035div#draft_list {
461 border: 1px solid #000000; 1036 border: 1px solid #000000;
462 padding: 5px; 1037 padding: 5px;
@@ -472,32 +1047,61 @@ div#draft_list table tr.header:hover {
472 background-color: #ffffff; 1047 background-color: #ffffff;
473} 1048}
474 1049
1050div#draft_list table tr:hover {
1051 background-color: #f1f1f1;
1052}
1053
475div#draft_list table td.actions { 1054div#draft_list table td.actions {
476 text-transform: lowercase; 1055 text-transform: lowercase;
477 white-space: nowrap; 1056 white-space: nowrap;
478} 1057}
479 1058
1059/* Layout only -- the at-rest visibility (wavy underline) for these
1060 links comes from the scoped rule in Base elements above. */
480div#draft_list table td.actions a { 1061div#draft_list table td.actions a {
481 letter-spacing: 1px; 1062 letter-spacing: 1px;
482 padding-left: 5px; 1063 padding-left: 5px;
483 padding-right: 5px; 1064 padding-right: 5px;
484} 1065}
485 1066
486div#draft_list table tr:hover { 1067.search_results_list {
487 background-color: #f1f1f1; 1068 list-style: none;
1069 margin: 0;
1070 padding: 0;
488} 1071}
489 1072
490table tr.header { 1073.search_results_list li {
491 height: 20px; 1074 padding: 0.5rem 0;
492 text-align: left; 1075 border-bottom: 1px solid #f1f1f1;
1076}
1077
1078.search_tag_row {
1079 display: flex;
1080 flex-wrap: wrap;
1081 gap: 0.4rem;
1082 padding: 4px 4px 8px;
1083 border-bottom: 1px solid #e8e8e8;
493} 1084}
494 1085
1086.search_tag_pill {
1087 display: inline-block;
1088 padding: 2px 10px;
1089 border-radius: 999px;
1090 background-color: #f1f1f1;
1091 font-size: 0.85rem;
1092 text-decoration: none;
1093}
1094
1095/* ============================================================
1096 Search widgets
1097 ============================================================ */
1098
495#search_widget { 1099#search_widget {
496 position: absolute; 1100 position: absolute;
497 top: 20px; 1101 top: 2.75rem;
498 left: 50%; 1102 left: 0;
499 transform: translateX(-50%);
500 width: min(520px, 90vw); 1103 width: min(520px, 90vw);
1104 border-radius: 2px;
501 border: 1px solid #000000; 1105 border: 1px solid #000000;
502 -webkit-box-shadow: 3px 3px 5px #b1b1b1; 1106 -webkit-box-shadow: 3px 3px 5px #b1b1b1;
503 box-shadow: 3px 3px 5px #b1b1b1; 1107 box-shadow: 3px 3px 5px #b1b1b1;
@@ -507,23 +1111,27 @@ table tr.header {
507} 1111}
508 1112
509#search_widget input { 1113#search_widget input {
510 width: calc(100% - 70px); 1114 width: 100%;
1115 box-sizing: border-box;
1116 padding: 8px 12px;
1117 border-radius: 2px;
511 font-size: 18px; 1118 font-size: 18px;
512} 1119}
513 1120
514#search_results p { 1121.search_results p {
515 margin: 0; 1122 margin: 0;
516 padding: 4px 4px 0 4px; 1123 padding: 4px 4px 0 4px;
517 border-bottom: 1px solid #e8e8e8; 1124 border-bottom: 1px solid #e8e8e8;
518} 1125}
519 1126
520#search_results p a { 1127.search_results p a {
1128 text-decoration: none;
521 display: block; 1129 display: block;
522 font-weight: bold; 1130 font-weight: bold;
523 font-size: 0.95rem; 1131 font-size: 0.95rem;
524} 1132}
525 1133
526#search_results p span.result_path { 1134.search_results p span.result_path {
527 display: block; 1135 display: block;
528 margin-top: 2px; 1136 margin-top: 2px;
529 font-size: 0.75rem; 1137 font-size: 0.75rem;
@@ -532,7 +1140,7 @@ table tr.header {
532 padding-bottom: 4px; 1140 padding-bottom: 4px;
533} 1141}
534 1142
535#search_results p.search_more { 1143.search_results p.search_more {
536 margin: 0; 1144 margin: 0;
537 padding: 6px 4px; 1145 padding: 6px 4px;
538 font-size: 0.8rem; 1146 font-size: 0.8rem;
@@ -541,13 +1149,19 @@ table tr.header {
541 border-bottom: none; 1149 border-bottom: none;
542} 1150}
543 1151
544table#content th.description { 1152.search_results p.search_group_label {
545 width: 100px; 1153 margin: 8px 0 2px;
1154 padding: 0;
1155 border-bottom: none;
1156 font-size: 0.75rem;
1157 color: #969696;
1158 text-transform: lowercase;
1159 font-weight: normal;
546} 1160}
547 1161
548table#content th.content { 1162/* ============================================================
549 width: 690px; 1163 Menu items (navigation editor)
550} 1164 ============================================================ */
551 1165
552#menu_item_list { 1166#menu_item_list {
553 border-collapse: collapse; 1167 border-collapse: collapse;
@@ -565,6 +1179,10 @@ table#content th.content {
565 height: 20px; 1179 height: 20px;
566} 1180}
567 1181
1182#menu_item_list td.menu_item_title {
1183 width: 200px;
1184}
1185
568#menu_item_list td.menu_sort_handle div { 1186#menu_item_list td.menu_sort_handle div {
569 background-color: #989898; 1187 background-color: #989898;
570 height: 26px; 1188 height: 26px;
@@ -583,14 +1201,14 @@ table#content th.content {
583 cursor: grab; 1201 cursor: grab;
584} 1202}
585 1203
586#menu_item_list td.menu_item_title { 1204.ui-state-highlight td {
587 width: 200px;
588}
589
590.ui-state-highlight td{
591 height: 20px; 1205 height: 20px;
592} 1206}
593 1207
1208/* ============================================================
1209 Image box / browser
1210 ============================================================ */
1211
594#metadata ul#image_box { 1212#metadata ul#image_box {
595 box-sizing: border-box; 1213 box-sizing: border-box;
596 margin: 0; 1214 margin: 0;
@@ -616,7 +1234,77 @@ div#image_browser ul li {
616 list-style-type: none; 1234 list-style-type: none;
617} 1235}
618 1236
619table.user_list td.user_login { 1237.thumbnail_list {
620 padding-right: 30px; 1238 display: flex;
1239 flex-wrap: wrap;
1240 gap: 0.75rem;
1241 list-style: none;
1242 margin: 0 0 0.75rem;
1243 padding: 0;
1244}
1245
1246.thumbnail_list li {
1247 position: relative;
1248 display: flex;
1249 align-items: center;
1250 gap: 0.25rem;
1251 border: 1px solid #e8e8e8;
1252 border-radius: 6px;
1253 padding: 4px;
1254}
1255
1256.thumbnail_list img {
1257 display: block;
1258 width: 60px;
1259 height: 60px;
1260 object-fit: cover;
1261 border-radius: 4px;
1262}
1263
1264.related_asset_handle {
1265 display: flex;
1266 color: #969696;
1267 cursor: grab;
1268}
1269
1270.related_asset_handle svg {
1271 width: 1rem;
1272 height: 1rem;
1273}
1274
1275.related_asset_remove {
1276 display: flex;
1277 align-items: center;
1278 justify-content: center;
1279 background: none;
1280 border: none;
1281 padding: 2px;
1282 cursor: pointer;
1283 color: #969696;
1284}
1285
1286.related_asset_remove svg {
1287 width: 0.9rem;
1288 height: 0.9rem;
1289}
1290
1291.related_asset_remove:hover {
1292 color: #cc0000;
1293}
1294
1295.related_asset_result {
1296 display: flex;
1297 align-items: center;
1298 gap: 0.5rem;
1299 padding: 4px;
1300 border-bottom: 1px solid #e8e8e8;
1301 text-decoration: none;
1302}
1303
1304.related_asset_result img {
1305 width: 40px;
1306 height: 40px;
1307 object-fit: cover;
1308 border-radius: 4px;
621} 1309}
622 1310
diff --git a/public/stylesheets/ccc.css b/public/stylesheets/ccc.css
index 30eb8dd..fe2144f 100644
--- a/public/stylesheets/ccc.css
+++ b/public/stylesheets/ccc.css
@@ -216,7 +216,7 @@ div#frontpage_calendar {
216 } 216 }
217} 217}
218 218
219div#frontpage_calendar h2, div#tags h2, div#featured_articles h2, div.main_navigation h2 { 219div#frontpage_calendar h2, div#tags h2, div#featured_articles h2, div#open_erfas_today h2, div.main_navigation h2 {
220 border-top: 2px solid; 220 border-top: 2px solid;
221 border-bottom: 2px solid; 221 border-bottom: 2px solid;
222 font-size: 1.1em; 222 font-size: 1.1em;
@@ -234,6 +234,18 @@ div#frontpage_calendar h2 {
234 margin-top: 0; 234 margin-top: 0;
235} 235}
236 236
237div#open_erfas_today li {
238 margin-top: 8px;
239 margin-bottom: 8px;
240 line-height: 1.25;
241}
242
243div#open_erfas_today .event_time {
244 font-style: italic;
245 font-family: Georgia;
246 color: color-mix(in srgb, CanvasText, #808080);
247}
248
237@media(max-width:1016px) { 249@media(max-width:1016px) {
238 div#tags li { 250 div#tags li {
239 list-style-type: none; 251 list-style-type: none;
@@ -248,7 +260,7 @@ div#frontpage_calendar h2 {
248} 260}
249 261
250@media(min-width:1016px) { 262@media(min-width:1016px) {
251 div#frontpage_calendar h2, div#tags h2, div#featured_articles h2 { 263 div#frontpage_calendar h2, div#tags h2, div#featured_articles h2, div#open_erfas_today h2 {
252 font-size: 1rem; 264 font-size: 1rem;
253 } 265 }
254 266
@@ -260,9 +272,9 @@ div#frontpage_calendar h2 {
260 272
261} 273}
262 274
263div#frontpage_calendar ul, div#tags ul, div#featured_articles ul { 275div#frontpage_calendar ul, div#tags ul, div#featured_articles ul, div#open_erfas_today ul {
264 padding: 0px; 276 padding: 0px;
265 font-size: 1rem; 277 font-size: 0.9rem;
266 line-height: 1.5em; 278 line-height: 1.5em;
267} 279}
268 280
@@ -278,11 +290,11 @@ div#frontpage_calendar li {
278 margin-bottom: 20px; 290 margin-bottom: 20px;
279} 291}
280 292
281div#frontpage_calendar li, div#tags li, div#featured_articles li { 293div#frontpage_calendar li, div#tags li, div#featured_articles li, div#open_erfas_today li {
282 list-style-type: none; 294 list-style-type: none;
283} 295}
284 296
285div#frontpage_calendar li a, div#tags li a, div#featured_articles li a { 297div#frontpage_calendar li a, div#tags li a, div#featured_articles li a, div#open_erfas_today li a {
286 text-decoration: none; 298 text-decoration: none;
287 color: color-mix(in srgb, CanvasText, #808080 25%); 299 color: color-mix(in srgb, CanvasText, #808080 25%);
288} 300}