#= require vendor_js class HatTrickWizard constructor: (formElem, @wizard) -> @form = $(formElem) this.enableFormwizard() debug: -> window.hatTrick.railsEnv == "development" linkClass: "_ht_link" buttons: [] stepsNeedUpdate: false stepShownCallback: -> currentStepId = this.currentStepId() @lastButtonChanged = null if hatTrick.stepMetadata[currentStepId]? hatTrick.metadata.currentStep = hatTrick.stepMetadata[currentStepId] else this.requestMetadataFromServer() return this.updateStepFromMetadata() currentStepId = this.currentStepId() # can't go back from the first step if hatTrick.metadata.currentStep.first this.buttons[currentStepId] = this.buttons[currentStepId].filter (button) -> not button.back? this.setupButtonsForCurrentStep() if @stepsNeedUpdate this.updateSteps() @stepsNeedUpdate = false else this.updateButtons() this.removeLinkFields() this.setFormFields(hatTrick.model) this.createDummyModelField() unless this.currentStepHasModelFields() @form.trigger 'step_changed', { currentStep: this.currentStepId() } addStepClass: -> @form.find("fieldset").addClass("step") findStep: (stepId) -> @form.find("fieldset##{stepId}") createMethodField: (method) -> """""" setAction: (url, method) -> methodLower = method.toLowerCase() @form.attr("action", url) @form.attr("method", "post") @form.formwizard("option", remoteAjax: this.ajaxEvents()) methodField = @form.find('input[name="_method"]') methodField.remove() if methodLower isnt "post" @form.prepend(this.createMethodField(method)) currentStepId: -> stepId = @form.formwizard("state").currentStep unless stepId? and stepId isnt "" stepId = hatTrick.metadata.currentStep.name stepId currentStep: -> stepId = this.currentStepId() this.findStep(stepId) fieldsets: -> @form.find("fieldset") ajaxEvents: -> remoteAjax = {} $fieldsets = this.fieldsets() $fieldsets.each (index, element) => stepId = $(element).attr("id") remoteAjax[stepId] = this.createAjaxEvent(stepId) remoteAjax createAjaxEvent: (step) -> ajax = url: @form.attr("action") dataType: "json" beforeSerialize: (form, options) => if options.data._ht_step_link == this.currentStepId() log "Warning: Tried to link to the current step; this is probably not what you want." return true; # beforeSubmit: (data) => # log "Sending these data to the server: #{JSON.stringify data}" success: (serverData) => this.clearErrors() this.handleServerData serverData @form.trigger 'ajaxSuccess', serverData # log "Successful form POST; got #{JSON.stringify(serverData)}" error: (event, status, errorThrown) => log "Error response: #{event.status} #{status} - #{errorThrown} - #{event.responseText}" try appErrors = eval "(#{event.responseText})" catch err appErrors = model: unknown: [ "There was an error communicating with the server. TurboVote staff have been notified." ] status: status event: event this.clearErrors() this.addErrorItem value[0] for key, value of appErrors.model when key isnt "__name__" this.removeLinkFields() @form.trigger 'ajaxErrors', appErrors ajax getErrorListElement: -> this.currentStep().find("ul.hat_trick_errors") clearErrors: -> $errorList = this.getErrorListElement() $errorList.hide() $errorList.empty() addErrorItem: (message) -> $errorList = this.getErrorListElement() if $errorList.length > 0 $errorList.append("
  • #{message}
  • ") $errorList.show() updateButtons: -> @form.formwizard("update_buttons") updateSteps: -> @form.formwizard("update_steps") @form.formwizard("option", remoteAjax: this.ajaxEvents()) goToStepId: (stepId) -> this.setLinkField(stepId) @form.formwizard("next") removeLinkFields: -> @form.find("input.#{@linkClass}").remove() setLinkField: (stepId) -> inputId = "_ht_link_to_#{stepId}" this.setHiddenInput "_ht_step_link", stepId, inputId, @linkClass, this.currentStep() linkFieldSet: -> this.currentStep().find("input[name='_ht_step_link']").length > 0 addFakeLastStep: -> @form.append """""" enableFormwizard: -> this.addStepClass() this.saveStepMetadata() this.setAction(hatTrick.metadata.url, hatTrick.metadata.method) # prevent submitting the step that happens to be the last fieldset # TODO: Figure out a better way to do this this.addFakeLastStep() this.bindEvents() firstStep = if hatTrick.metadata.currentStep?.redirect hatTrick.metadata.currentStep.redirectFrom else hatTrick.metadata.currentStep.fieldset @form.formwizard formPluginEnabled: true, validationEnabled: false, focusFirstInput: true, disableUIStyles: true, outDuration: 200, inDuration: 10, # don't set this to 0 or step_shown will be triggered too early next: "button:submit", back: "button:reset", linkClass: ".#{@linkClass}", remoteAjax: this.ajaxEvents(), firstStep: firstStep # see if we got a redirect & follow if so currentStepId = this.currentStepId() if hatTrick.metadata.currentStep? currentStepData = hatTrick.metadata.currentStep if currentStepData.redirect and currentStepData.redirectFrom is currentStepId @form.formwizard("redirect", currentStepData.fieldset) setHiddenInput: (name, value, id, classes = "", scope = @form) -> $scope = $(scope) $input = $scope.find("""input[name="#{name}"]""") if $input.length is 0 $input = $(this.hiddenInputHTML(name, id, classes)).prependTo $scope $input.val value $input hiddenInputHTML: (name, id, classes = "") -> """""" setHTMeta: (key, value) -> this.setHiddenInput "_ht_meta[#{key}]", value, "_ht_#{key}" clearHTMeta: (key) -> @form.find("input:hidden#_ht_#{key}").remove() setCurrentStepField: -> stepId = this.currentStepId() this.setHTMeta("step", stepId) fieldRegex: /^([^\[]+)\[([^\]]+)\]$/ setFieldValues: (model, selector, callback) -> $currentStep = this.currentStep() $currentStep.find(selector).each (index, element) => $element = $(element) elementName = $element.attr("name") if elementName? and elementName.search(@fieldRegex) isnt -1 [_, modelName, fieldName] = elementName.match(@fieldRegex) if model['__name__'] is modelName and model[fieldName]? fieldValue = model[fieldName] callback($element, fieldValue) if fieldValue? fillTextFields: (model) -> this.setFieldValues model, "input:text", ($input, value) => $input.val(value) setSelectFields: (model) -> this.setFieldValues model, "select", ($select, value) => $select.find("option[value=\"#{value}\"]").attr("selected", "selected") setCheckboxes: (model) -> this.setFieldValues model, "input:checkbox", ($checkbox, value) => $checkbox.attr("checked", "checked") if value # TODO: DRY this up as much as possible. Radio buttons a little different # than the other form controls since they share names and behave as a # named group. setRadioButtons: (model) -> $currentStep = this.currentStep() selector = "input:radio" radioGroups = {} $currentStep.find(selector).each -> radioGroups[$(this).attr("name")] = true for radioGroup of radioGroups do (radioGroup) => if radioGroup.search(@fieldRegex) isnt -1 [_, modelName, fieldName] = radioGroup.match(@fieldRegex) if model['__name__'] is modelName and model[fieldName]? fieldValue = model[fieldName] $radioGroup = $("input:radio[name=\"#{radioGroup}\"]") $radioGroup.removeAttr("checked") $radioGroup.filter("[value=\"#{fieldValue}\"]").attr("checked", "checked") setFormFields: (model) -> this.fillTextFields(model) this.setSelectFields(model) this.setCheckboxes(model) this.setRadioButtons(model) createButtonElement: (name, value, label, type="button") -> $elem = $("""""") $elem.attr "name", name if name? $elem.html label $elem.val value if value? $elem createButton: (toStep, button) -> switch toStep when "next" if button.class is "" button.class = "wizard_next" else button["class"] += " wizard_next" type = "submit" when "back" if button.class is "" button.class = "wizard_back" else button["class"] += " wizard_back" button.name = "back" unless button.id? button.id = "#{this.currentStepId()}_back_button" delete button["value"] type = "reset" else type = "button" $button = this.createButtonElement button.name, button.value, button.label, type if button.id? $button.attr("id", button.id) else if button.name? and button.value? $button.attr("id", "#{this.currentStepId()}_#{button.name}_#{button.value}") if button["class"]? $button.addClass(button["class"]) $button setButton: (stepId, toStep, button) -> $buttonsDiv = $("fieldset##{stepId}").find("div.buttons") if $buttonsDiv.find("button").length > 0 @lastButtonChanged ?= $buttonsDiv.find("button:first") buttonSelector = """button[name="#{button.name}"][value="#{button.value}"]""" $existingButtons = $buttonsDiv.find(buttonSelector) if $existingButtons.length is 0 $newButton = $(this.createButton(toStep, button)) if @lastButtonChanged? @lastButtonChanged.after $newButton else $buttonsDiv.append $newButton @lastButtonChanged = $newButton unless toStep is "next" or toStep is "back" $newButton.click (event) => event.preventDefault() fieldId = "button_#{$newButton.attr("name")}_#{$newButton.val()}_field" this.setHiddenInput $newButton.attr("name"), $newButton.val(), fieldId, "", $buttonsDiv this.goToStepId(toStep) setupButtonsForCurrentStep: -> $currentStep = this.currentStep() $buttonsDiv = $currentStep.find("div.buttons") $buttons = $buttonsDiv.find("button") unless $buttonsDiv.data "buttonsAdded" this.setupButtonsForStep this.currentStepId() setupButtonsForStep: (stepId) -> buttons = this.buttons[stepId] if buttons? for button in buttons do (button) => this.setButton(stepId, toStep, buttonData) for toStep, buttonData of button $("##{stepId} div.buttons").data "buttonsAdded", true setContents: (stepPartials) -> for stepName, partial of stepPartials do (stepName, partial) => stepId = underscoreString stepName $partial = $(partial) fieldsetContents = if $partial.filter("fieldset").length > 0 $partial.filter("fieldset").html() else $partial.find('fieldset').html() $step = $("fieldset##{stepId}") $step.html fieldsetContents $step.filter("fieldset:not(.no-focus)").find(":input:not(input[type=hidden]):first").focus(); $step.data("contents", "loaded") @stepsNeedUpdate = true saveStepMetadata: (stepId=this.currentStepId(), metadata=hatTrick.metadata.currentStep) -> hatTrick.stepMetadata = {} unless hatTrick.stepMetadata? hatTrick.stepMetadata[stepId] = metadata handleServerData: (data) => if data.metadata?.externalRedirectURL? externalRedirectURL = data.metadata.externalRedirectURL if externalRedirectURL isnt "" location.href = data.metadata.externalRedirectURL # TODO: pop out of window if in iframe # if (top.location == self.location) # location.href = data.metadata.externalRedirectURL # else # window.open(data.metadata.externalRedirectURL) if data.metadata?.url? and data.metadata?.method? this.setAction(data.metadata.url, data.metadata.method) this.saveStepMetadata(data.metadata.currentStep.name, data.metadata.currentStep) $.extend(hatTrick, data) # merge new data with hatTrick this.updateStepFromMetadata() metadataRequestCallback: (data) => stepId = this.currentStepId() # set empty step contents if we didn't get any; # this makes sure we can tell whether or not we've already requested metadata emptyStepContents = { hatTrickStepContents: {} } stepKey = camelizeString(stepId) emptyStepContents["hatTrickStepContents"][stepKey] = "" unless data.data.hatTrickStepContents? and data.data.hatTrickStepContents[stepKey]? data.data = $.extend({}, data.data, emptyStepContents) this.handleServerData(data) this.removeLinkFields() # updateStepFromMetadata sets this to currentStep this.setupButtonsForCurrentStep() this.updateButtons() this.setFormFields(hatTrick.model) @form.trigger "step_changed", { currentStep: stepId } requestMetadataFromServer: -> metadataUrl = document.location.pathname lastChar = metadataUrl.charAt(metadataUrl.length - 1) metadataUrl += "/" unless lastChar is "/" stepId = this.currentStepId() metadataUrl += "#{stepId}/" if metadataUrl.search("#{stepId}/$") is -1 metadataUrl += "metadata" $.ajax type: "GET" url: metadataUrl success: this.metadataRequestCallback dataType: "json" updateStepContents: -> stepKey = camelizeString(this.currentStepId()) if hatTrick.data?.hatTrickStepContents?[stepKey]? this.setContents(hatTrick.data.hatTrickStepContents) else this.requestMetadataFromServer() setButtonMetadataForCurrentStep: -> if hatTrick.metadata?.currentStep? currentStep = hatTrick.metadata.currentStep if currentStep.buttons? stepId = currentStep.fieldset this.buttons[stepId] = currentStep.buttons modelName: -> hatTrick.model['__name__'] createDummyModelField: -> this.setHiddenInput "#{this.modelName()}[_dummy]", "1", "", this.currentStep() currentStepHasModelFields: -> this.currentStep().find("input[name^='#{this.modelName()}[']").length > 0 updateStepFromMetadata: -> currentStepId = this.currentStepId() if $("fieldset##{currentStepId}").data("contents") is "server" this.updateStepContents() if hatTrick.metadata?.currentStep? currentStepData = hatTrick.metadata.currentStep this.setCurrentStepField() this.setButtonMetadataForCurrentStep() this.createDummyModelField() unless this.currentStepHasModelFields() this.setLinkField(currentStepData.fieldset) bindEvents: -> @form.bind "step_shown", (event, data) => this.stepShownCallback() $ -> if $("form.wizard").length > 0 $form = $("form.wizard") window.hatTrick ?= {} unless window.hatTrick.wizard? window.hatTrick.wizard = new HatTrickWizard($form, hatTrick.metadata) camelizeString = (string) -> re = /_([^_]*)/g while matches = re.exec(string) result = string.slice(0, matches.index) unless result? result += "#{matches[1][0].toUpperCase()}#{matches[1].slice(1)}" result = string unless result? result underscoreString = (string) -> re = /([A-Z]+)([a-z\d]+)/g while matches = re.exec(string) result = string.slice(0, matches.index) unless result? result += "_#{matches[1].toLowerCase()}#{matches[2]}" result = string unless result? result log = (msg) -> if window['console']? and hatTrick.wizard.debug() console.log msg