summaryrefslogtreecommitdiff
path: root/public/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'public/javascripts')
-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
4 files changed, 481 insertions, 1357 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};