diff --git a/app/client/lib/form_is_dirty.js b/app/client/lib/form_is_dirty.js new file mode 100644 index 0000000..78274a3 --- /dev/null +++ b/app/client/lib/form_is_dirty.js @@ -0,0 +1,35 @@ +/** + * Determines if a form is dirty by comparing the current value of each element + * with its default value. + * + * @param {Form} form the form to be checked. + * @return {Boolean} true if the form is dirty, false + * otherwise. + */ +formIsDirty = function formIsDirty(form) { + for (var i = 0; i < form.elements.length; i++) { + var element = form.elements[i]; + var type = element.type; + if (type == "checkbox" || type == "radio") { + if (element.checked != element.defaultChecked) { + return true; + } + } + else if (type == "hidden" || type == "password" || + type == "text" || type == "textarea" || + type == "number" ) { + if (element.value != element.defaultValue) { + return true; + } + } + else if (type == "select-one" || type == "select-multiple") { + for (var j = 0; j < element.options.length; j++) { + if (element.options[j].selected != + element.options[j].defaultSelected) { + return true; + } + } + } + } + return false; +} diff --git a/app/client/views/autoform/textarea.coffee b/app/client/views/autoform/textarea.coffee new file mode 100644 index 0000000..076ff40 --- /dev/null +++ b/app/client/views/autoform/textarea.coffee @@ -0,0 +1,33 @@ +#work around autoform's missing defaultValues +textareaRendered = -> + e = @$('textarea')[0] + e.defaultValue = e.value + return + +Template.afTextarea.rendered = textareaRendered +Template.afTextarea_bootstrap3.rendered = textareaRendered + + +inputRendered = -> + e = @$('input')[0] + e.defaultValue = e.value + return + +Template.afInputNumber.rendered = inputRendered +Template.afInputNumber_bootstrap3.rendered = inputRendered + +afBootstrapDatepickerRendered = Template.afBootstrapDatepicker.rendered +Template.afBootstrapDatepicker.rendered = -> + afBootstrapDatepickerRendered.call @ + self = @ + @autorun -> + data = Template.currentData() + inputRendered.call self + +afBootstrapDateTimePickerRendered = Template.afBootstrapDateTimePicker.rendered +Template.afBootstrapDateTimePicker.rendered = -> + afBootstrapDateTimePickerRendered.call @ + self = @ + @autorun -> + data = Template.currentData() + inputRendered.call self diff --git a/app/client/views/patients/patient_visit.coffee b/app/client/views/patients/patient_visit.coffee index e27d319..2d8305f 100644 --- a/app/client/views/patients/patient_visit.coffee +++ b/app/client/views/patients/patient_visit.coffee @@ -1,47 +1,47 @@ waitingForPatientId = null waitingForDesignVisitId = null Template.patientVisit.rendered = -> @autorun -> data = Template.currentData() patientId = data.patient._id designVisitId = Session.get 'selectedDesignVisitId' return if not designVisitId? v = Visits.findOne designVisitId: designVisitId if not v? and (waitingForPatientId isnt patientId or waitingForDesignVisitId isnt designVisitId) #console.log 'initVisit' waitingForPatientId = patientId waitingForDesignVisitId = designVisitId Meteor.call "initVisit", designVisitId, patientId, (error, _id) -> throwError error if error? Template.patientVisit.helpers #this templateData visit: -> designVisitId = Session.get 'selectedDesignVisitId' v = Visits.findOne designVisitId: designVisitId v.validatedDoc() if v? #with questionnaire=this visit=.. patient=../../patient questionnaireCSS: -> return "valid" if @questionnaire.answered "invalid" Template.patientVisit.events #with questionnaire=this visit=.. patient=../../patient "click .answerQuestionnaire": (evt, tmpl) -> - Modal.show('questionnaireWizzard', @) + Modal.show('questionnaireWizzard', @, keyboard: false) false #this: {questionnaire, visit, patient} "click .showQuestionnaire": (evt, tmpl) -> data = questionnaire: @questionnaire visit: @visit patient: @patient readonly: true - Modal.show('questionnaireWizzard', data) + Modal.show('questionnaireWizzard', data, keyboard: false) false diff --git a/app/client/views/questionnaires/questionnaire_wizzard.coffee b/app/client/views/questionnaires/questionnaire_wizzard.coffee index 28a5816..cfaa4a9 100644 --- a/app/client/views/questionnaires/questionnaire_wizzard.coffee +++ b/app/client/views/questionnaires/questionnaire_wizzard.coffee @@ -1,249 +1,317 @@ _numQuestions = new ReactiveVar(0) _numPages = new ReactiveVar(0) _questionIdsForPage = new ReactiveVar({}) _pageIndex = new ReactiveVar(0) _numFormsToSubmit = 0 _readonly = ReactiveVar(false) -nextPage = -> - if _pageIndex.get() is _numPages.get()-1 - Modal.hide('viewQuestionnaire') - else - _pageIndex.set _pageIndex.get()+1 -previousPage = -> - index = _pageIndex.get() - index -= 1 if index > 0 - _pageIndex.set index +isAFormDirty = -> + if _readonly.get() + return false + isDirty = false + $("form").each () -> + return if isDirty + e = $(@)[0] + dirty = formIsDirty(e) + isDirty = dirty if dirty + isDirty -_gotoNextPage = null -submitAllForms = (gotoNextPage) -> +_goto = null +submitAllForms = (goto) -> if _readonly.get() throw new Error("Can't submitAllForms because _readonly == true") - _gotoNextPage = gotoNextPage + _goto = goto numFormsToSubmit = 0 $("form").each () -> e = $(@) classes = e.attr('class') if classes? and classes.indexOf('question') > -1 numFormsToSubmit += 1 _numFormsToSubmit = numFormsToSubmit $("form").each () -> e = $(@) classes = e.attr('class') if classes? and classes.indexOf('question') > -1 e.submit() formSubmitted = -> if (_numFormsToSubmit -= 1) <= 0 - if _gotoNextPage + if _goto is 'nextPage' nextPage() - else + else if _goto is 'previousPage' previousPage() + else if _goto is 'close' + Modal.hide('questionnaireWizzard') + else if _goto.pageIndex? + _pageIndex.set _goto.pageIndex + + +close = -> + if isAFormDirty() + swal { + title: 'Unsaved Changes' + text: "Do you want to save the changes on this page?" + type: 'warning' + showCancelButton: true + confirmButtonText: 'Save and exit' + cancelButtonText: "Exit without saving" + }, (save) -> + if save + submitAllForms('close') + else + Modal.hide('questionnaireWizzard') + else + Modal.hide('questionnaireWizzard') + + +nextPage = -> + if _pageIndex.get() is _numPages.get()-1 + Modal.hide('questionnaireWizzard') + else + _pageIndex.set _pageIndex.get()+1 + +previousPage = -> + index = _pageIndex.get() + index -= 1 if index > 0 + _pageIndex.set index autoformHooks = onSubmit: (insertDoc, updateDoc, currentDoc) -> insertDoc.visitId = currentDoc.visitId insertDoc.questionId = currentDoc.questionId insertDoc._id = currentDoc._id if currentDoc._id? #console.log "submit questionAutoform" #console.log insertDoc if insertDoc.value? and (!currentDoc.value? or (currentDoc.value? and currentDoc.value isnt insertDoc.value)) Meteor.call "upsertAnswer", insertDoc, (error) -> throwError error if error? formSubmitted() @done() false Template.questionnaireWizzard.created = -> @subscribe("questionsForQuestionnaire", @data.questionnaire._id) + if @data.readonly _readonly.set true else _readonly.set false + + #close on escape key press + $(document).on('keyup.wizzard', (e)-> + e.stopPropagation() + if e.keyCode is 27 + close() + return + ) + + #collect autoformIds, count pages self = @ @autorun -> count = 0 page = 0 questionIdsForPage = {} didBreakPage = false autoformIds = [] Questions.find questionnaireId: self.data.questionnaire._id , sort: {index: 1} .forEach (q) -> if q.type isnt "description" and q._id isnt "table" and q._id isnt "table_polar" autoformIds.push q._id count += 1 if questionIdsForPage[page]? questionIdsForPage[page].push q._id else questionIdsForPage[page] = [q._id] didBreakPage = false if q.break page += 1 didBreakPage = true page -= 1 if didBreakPage _questionIdsForPage.set questionIdsForPage _numQuestions.set count _numPages.set page+1 _pageIndex.set 0 AutoForm.addHooks(autoformIds, autoformHooks) +Template.questionnaireWizzard.destroyed = -> + $(document).unbind('keyup.wizzard') + Template.questionnaireWizzard.helpers templateGestures: 'swipeleft div': (evt, templateInstance) -> nextQuestion() 'swiperight div': (evt, templateInstance) -> previousQuestion() questionsForPage: -> questionIdsForPage = _questionIdsForPage.get()[_pageIndex.get()] Questions.find questionnaireId: @questionnaire._id _id: {$in: questionIdsForPage} , sort: {index: 1} answerForQuestion: (visitId, questionId) -> Answers.findOne visitId: visitId questionId: questionId readonly: -> _readonly.get() formType: -> if _readonly.get() "disabled" else "normal" answerFormSchema: -> schema = _id: type: String optional: true visitId: type: String optional: true questionId: type: String optional: true value: @question.getSchemaDict() new SimpleSchema(schema) doc: -> @answer or visitId: @visit._id questionId: @question._id pages: -> answers = {} questionIds = Questions.find questionnaireId: @questionnaire._id .map (question) -> question._id Answers.find visitId: @visit._id questionId: {$in: questionIds} .forEach (answer) -> answers[answer.questionId] = answer activeIndex = _pageIndex.get() questionIdsForPage = _questionIdsForPage.get() pages = [] for i in [0.._numPages.get()-1] css = "" allQuestionsAnsweredInPage = true someQuestionsAnsweredInPage = false Questions.find questionnaireId: @questionnaire._id _id: {$in: questionIdsForPage[i]} .forEach (question) -> return if question.type is "description" answer = answers[question._id] if question.type is "table" or question.type is "table_polar" or question.type is "multipleChoice" if !answer? or answer.value.length < question.subquestions.length allQuestionsAnsweredInPage = false if answer? and answer.value.length > 0 someQuestionsAnsweredInPage = true else if !answer? allQuestionsAnsweredInPage = false if allQuestionsAnsweredInPage css = "answered" else if someQuestionsAnsweredInPage css = "answeredPartly" if i is activeIndex css += " active" pages[i] = index: i+1 css: css pages isOnFirstPage: -> _pageIndex.get() is 0 isOnLastPageOfLastQuestionnaire: -> validatedQuestionnaires = @visit.validatedQuestionnaires _pageIndex.get() is _numPages.get()-1 and @questionnaire._id is validatedQuestionnaires[validatedQuestionnaires.length-1]._id Template.questionnaireWizzard.events "click #next": (evt, tmpl) -> if _readonly.get() nextPage() else - submitAllForms(true) + submitAllForms('nextPage') false "click #back": (evt, tmpl) -> if _readonly.get() previousPage() else - submitAllForms(false) + submitAllForms('previousPage') false "click .jumpToPage": (evt) -> - _pageIndex.set @index-1 + pageIndex = @index-1 + if isAFormDirty() + swal { + title: 'Unsaved Changes' + text: "Do you want to save the changes on this page?" + type: 'warning' + showCancelButton: true + confirmButtonText: 'Save' + cancelButtonText: "Don't save" + }, (save) -> + if save + submitAllForms(pageIndex: pageIndex) + else + _pageIndex.set pageIndex + else + _pageIndex.set pageIndex false - + + "click #close": (evt) -> + close() + false + "submit .questionForm": (evt) -> if _readonly.get() return evt.preventDefault() evt.stopPropagation() if @question.type is "description" formSubmitted() return answer = visitId: @visit._id questionId: @question._id value: [] _id: @answer._id if @answer? for subquestion in @question.subquestions inputs = $(evt.target).find("input[data-subquestion_code=#{subquestion.code}]:checked") checkedChoices=[] inputs.each -> #checked choices input = $(@) checkedChoices.push value: input.data('choice_value').toString() variable: input.data('choice_variable').toString() if checkedChoices.length > 0 answer.value.push code: subquestion.code checkedChoices: checkedChoices if answer.value.length > 0 Meteor.call "upsertAnswer", answer, (error) -> throwError error if error? formSubmitted() else formSubmitted() false diff --git a/app/client/views/questionnaires/questionnaire_wizzard.html b/app/client/views/questionnaires/questionnaire_wizzard.html index 93d63f8..3328c24 100644 --- a/app/client/views/questionnaires/questionnaire_wizzard.html +++ b/app/client/views/questionnaires/questionnaire_wizzard.html @@ -1,75 +1,75 @@