Skip to content

Commit 0801d57

Browse files
authored
Merge pull request #450 from DigitalCurationCentre/issue_438
auto-saving functionality. #438
2 parents f653f1f + adbef72 commit 0801d57

File tree

7 files changed

+108
-12
lines changed

7 files changed

+108
-12
lines changed

app/views/answers/_new_edit.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
This partial creates a form for each type of question. The local variables are: plan, answer, question, readonly
33
-->
44
<% q_format = question.question_format %>
5-
<%= semantic_form_for answer, :url => {controller: :answers, action: :update }, html: {method: "put", class: "roadmap-form"}, remote: true do |f| %>
5+
<%= semantic_form_for answer, :url => {controller: :answers, action: :update }, html: {method: "put", class: "roadmap-form", 'data-autosave': question.id }, remote: true do |f| %>
66
<fieldset class="standard">
77
<% if !readonly %>
88
<%= f.input :id, as: :hidden, input_html: { value: answer.id } %>
@@ -74,7 +74,7 @@
7474
<p><%= raw(answer.text) %></p>
7575
<% else %>
7676
<%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}") %>
77-
<%= tinymce(selector: "#answer-text-#{question.id}", setup: "$.fn.change_answer", content_css: asset_path('application.css')) %>
77+
<%= tinymce(selector: "#answer-text-#{question.id}", setup: "$.fn.tinymce_answer_events", content_css: asset_path('application.css')) %>
7878
<% end %>
7979
<%end%>
8080
<% end %>
@@ -90,7 +90,7 @@
9090
<p><%= raw(answer.text) %></p>
9191
<% else %>
9292
<%= text_area_tag('answer[text]', answer.text, id: "answer-text-#{question.id}") %>
93-
<%= tinymce(selector: "#answer-text-#{question.id}", setup: "$.fn.change_answer", content_css: asset_path('application.css')) %>
93+
<%= tinymce(selector: "#answer-text-#{question.id}", setup: "$.fn.tinymce_answer_events", content_css: asset_path('application.css')) %>
9494
<% end %>
9595
<% end %>
9696

app/views/phases/_answer_form.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
end
1919
%>
2020
<div class="question-form">
21-
<div id="<%= "answer-locking-#{question.id}" %>"></div>
21+
<div id="<%= "answer-locking-#{question.id}" %>" class="answer-locking"></div>
2222
<div id="<%= "answer-form-#{question.id}" %>">
2323
<%= render(partial: 'answers/new_edit', locals: { question: question, answer: answer, readonly: readonly }) %>
2424
</div>

app/views/phases/edit.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<%- model_class = Plan -%>
2-
<% javascript "plans.js" %>
3-
<% javascript "answers/status.js" %>
4-
<% javascript "notes/index.js" %>
2+
<% javascript('plans.js') %>
3+
<% javascript('answers/status.js') %>
4+
<% javascript ('notes/index.js') %>
55
<!--
66
editing plan details is handled through plan#show
77
so if we come this way then we are editing a phase

config/application.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class Application < Rails::Application
8686
config.assets.precompile += %w(answers/status.js)
8787
config.assets.precompile += %w(notes/index.js)
8888
config.assets.precompile += %w(bootstrap_listeners.js)
89+
config.assets.precompile += %w(Dmproadmap.js)
8990

9091
config.autoload_paths += %W(#{config.root}/lib)
9192
config.action_controller.include_all_helpers = true
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
window.DMPROADMAP = (function(){
2+
return {
3+
/*
4+
Delays invoking of the function passed until after wait milliseconds have elapsed since
5+
the last time the debounced function was invoked.
6+
@param {function} func - the function to execute later on
7+
@param {number} wait - the number of milliseconds to wait until func is executed
8+
@returns The debounced function. It comes with a cancel method to cancel delayed func invocation
9+
*/
10+
debounce: function(func, wait){
11+
var timeoutID = null;
12+
function cancel() {
13+
if(timeoutID !== null){
14+
clearTimeout(timeoutID);
15+
return true;
16+
}
17+
return false;
18+
}
19+
return (function() {
20+
var debounced = function() {
21+
var ctx = this;
22+
var args = arguments;
23+
var later = function() {
24+
timeoutID = null;
25+
func.apply(ctx, args);
26+
}
27+
clearTimeout(timeoutID);
28+
timeoutID = setTimeout(later, wait || 5000);
29+
}
30+
debounced.cancel = cancel;
31+
return debounced;
32+
})();
33+
}
34+
};
35+
})();
Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,86 @@
11
//= require jquery.timeago.js
22

33
$(document).ready(function(){
4-
$("form.answer").submit(function(){
4+
/*--------------
5+
START Autosaving
6+
----------------*/
7+
// debounced object holds a set of debounced functions, one for each form present in the page. Note,
8+
// each debounced function stored at funcs is created on demand, i.e. once the user changes any element of a form
9+
var debounced = (function(){
10+
var funcs = {};
11+
return {
12+
has: function(id){
13+
return funcs[id] !== undefined;
14+
},
15+
get: function(id){
16+
17+
return funcs[id];
18+
},
19+
set: function(id, func){
20+
funcs[id] = DMPROADMAP.debounce(func, 5000);
21+
}
22+
}
23+
})();
24+
// This function triggers a form submit, if and only if the answer has not been optimistically locked
25+
function autoSaving(){
26+
if($(this).closest('.question-form').find('.answer-locking').children().length === 0){
27+
$(this).closest('form.answer').submit();
28+
}
29+
};
30+
/*--------------
31+
END Autosaving
32+
----------------*/
33+
// Listener for submit event triggered
34+
$('.question-form').on('submit', 'form.answer', function(){
35+
var id = $(this).attr('data-autosave');
36+
if(debounced.has(id)){
37+
debounced.get(id).cancel(); //Cancels the execution of its debounced function, if not already, since submit() could have been trigerred through Save button
38+
}
539
var container = $(this).closest('.question-form');
640
var saving = container.find('.saving-message');
741
saving.show();
842
});
9-
$("form.answer fieldset input, form.answer fieldset select").change(function(){
43+
// Listener for changes at any element value from question-form
44+
$('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', function(){
1045
var unsaved = $(this).closest('.question-form').find('.answer-unsaved');
1146
unsaved.show();
1247
var notAnswered = $(this).closest('.question-form').find('.not-answered');
1348
notAnswered.hide();
1449
});
15-
$.fn.change_answer = function(editor){
16-
editor.on('change', function(event){
50+
// Listener for changes at any element value from question-form. This triggers the debounced function
51+
$('.question-form').on('change', 'form.answer fieldset input, form.answer fieldset select', function(){
52+
var id = $(this).closest('form.answer').attr('data-autosave');
53+
if(!debounced.has(id)){
54+
debounced.set(id, autoSaving);
55+
}
56+
debounced.get(id).apply($(this),[id]);
57+
});
58+
// Function bounded to Jquery scope that setup event handlers for tinymce instances
59+
$.fn.tinymce_answer_events = function(editor){
60+
editor.on('change', function(){
1761
var unsaved = $('#'+editor.id).closest('.question-form').find('.answer-unsaved');
1862
unsaved.show();
1963
var notAnswered = $('#'+editor.id).closest('.question-form').find('.not-answered');
2064
notAnswered.hide();
2165
});
66+
editor.on('blur', function(){
67+
var id = $('#'+editor.id).closest('form.answer').attr('data-autosave');
68+
$('#'+editor.id).val($('#'+editor.id).tinymce().getContent()); // Forces Updating content textarea with the value from tinymce
69+
if(!debounced.has(id)){
70+
debounced.set(id, autoSaving);
71+
}
72+
debounced.get(id).apply($('#'+editor.id),[id]);
73+
});
74+
editor.on('focus', function(){
75+
var id = $('#'+editor.id).closest('form.answer').attr('data-autosave');
76+
if(debounced.has(id)){
77+
debounced.get(id).cancel(); //Cancels the execution of its debounced function either because user transitioned from question with options
78+
// to the comments or because textarea lost focus and gained again before the delay being met
79+
}
80+
});
2281
}
2382
$.fn.init_answer_status = function() {
24-
$('abbr.timeago').timeago();
83+
$('abbr.timeago').timeago(); //TODO examine if possible refactoring as event-delegated
2584
}
2685
$.fn.init_answer_status();
2786
});

lib/assets/javascripts/application.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@
2323
//= require gettext/all
2424
//= require jquery-accessible-autocomplet-list-aria.js
2525
//= require bootstrap_listeners.js
26+
//= require Dmproadmap.js

0 commit comments

Comments
 (0)