/**
* QuestionNaming.js 1.1
*
* (c) 2013 JotForm Easiest Form Builder
*
* QuestionNaming.js may be freely distributed under the MIT license.
* For all details and documentation:
* http://api.jotform.com
*/
/*
* INITIAL SETUP
* =============
*
* Include JotForm.js script to your page
* ____
*
* Include JotFormQuestionNaming.js script to your page
* ____
*/
(function (base) {
var QuestionNaming = function()
{
this.settings =
{
sort: 'order',
sortType: 'ASC',
title: 'Question Naming',
initial_data: false,
remember: true,
ignore_types: [
"control_head",
"control_button",
"control_pagebreak",
"control_collapse",
"control_text"
],
unique: false,
unique_error_msg: "You cannot name fields with the same name.",
allowed_inputs: /^[a-z0-9_]+$/i,
inputs_error_msg: "Only Alphabetic and Numeric characters are allowed.",
onSubmit: function(){},
onReady: function(){},
onClose:function(){},
onLoad: function(){}
};
this.overlayCSS = '//js.jotform.com/css/JotFormWidgetsCommon.css';
this.modalCSS = '//js.jotform.com/css/JotFormQuestionNaming.css';
this.jqueryLib = '//js.jotform.com/vendor/jquery-1.9.1.min.js';
this.toolScript = '//js.jotform.com/vendor/Tools.js';
//common styles for modal header, content, footer
this.commonCSS = '//js.jotform.com/css/JotFormModalCommon.css';
this.listTemplate =
'
' +
'
<%=text%>
' +
'
' +
'' +
'' +
'
' +
'
';
this.errorInputs = {};
this.uniqueErrorInputs = {};
this.loadCSS = function(docEl, url, onLoad)
{
//check if css already included
var ss = document.styleSheets;
for (var i = 0, max = ss.length; i < max; i++) {
var currSS = null;
if (ss[i] && ss[i].href && String(ss[i].href).indexOf('?rev=') > -1) {
currSS = String(ss[i].href).split('?rev=')[0];
}
if (currSS == url) {
onLoad();
}
}
var styleElement = document.createElement('link');
styleElement.setAttribute('type', 'text/css');
styleElement.setAttribute('rel', 'stylesheet');
styleElement.setAttribute('href', url + '?rev=' + new Date().getTime());
docEl.getElementsByTagName('head')[0].appendChild(styleElement);
if (styleElement.readyState) { //IE
styleElement.onreadystatechange = function () {
if (styleElement.readyState == "loaded" || styleElement.readyState == "complete") {
styleElement.onreadystatechange = null;
onLoad();
}
};
} else { //Others
//if safari and not chrome fire onload instantly - chrome has a safari string on userAgent
//this is a bug fix on safari browsers, having a problem on onload of an element
if (navigator.userAgent.match(/safari/i) && !navigator.userAgent.match(/chrome/i)) {
onLoad();
} else {
styleElement.onload = onLoad();
}
}
};
this.loadJS = function(docEl, docWindow)
{
var self = this
, s = docEl.createElement( 'script' );
var ucfirst = function (str) {
str += '';
var f = str.charAt(0).toUpperCase();
return f + str.substr(1);
};
s.setAttribute( 'src', self.jqueryLib );
docEl.body.appendChild( s );
s.onload = s.onreadystatechange = function(){
//load eventtarget/template script
var s2 = docEl.createElement( 'script' );
s2.setAttribute( 'src', self.toolScript );
docEl.body.appendChild( s2 );
s2.onload = s2.onreadystatechange = function() {
//waits for dom/script load
var timer = setInterval(function() {
if (docWindow.questionList !== undefined)
{
clearInterval(timer);
if ( self.settings.remember )
{
//load the fields from previous if any and remember it once save
self.showMessage(self.widgetDocument, "Reading previous entered data, please wait...");
self.fetchCustomFields(function(){
self.renderList(docWindow);
});
}
else
{
//dont mind, show defaults
self.renderList(docWindow);
}
}
},400); //IE needs a little more time to load the script
}
//after jQuery loaded
//TODO: ready function when all scripts loaded
// $(function(){
// });
};
};
/**
* Render the list on the modal dialog
*/
this.renderList = function(docWindow)
{
var self = this;
docWindow.target = new docWindow.EventTarget();
var markup ='
' +
'
' +
'
' +
'
JotForm Fields
' +
'
Custom Fields
' +
'
';
//sort questions, if any
var keySort = self.settings.sort;
docWindow.questionList.sort(function(a,b) {
if ( self.settings.sortType == 'DESC' ) {
return parseInt(b[keySort]) <= parseInt(a[keySort]) ? -1 : 1;
} else if ( self.settings.sortType == 'ASC' ) {
return parseInt(b[keySort]) >= parseInt(a[keySort]) ? -1 : 1;
}
});
//render them all
for (var i = 0; i < docWindow.questionList.length; i++)
{
var question = docWindow.questionList[i];
question['modified_text'] = (typeof question['modified_text'] !== 'undefined') ? question['modified_text'] : question['text'];
var item = docWindow.tmpl(self.listTemplate, question);
markup += item;
}
markup = markup + '
' + '
';
docWindow.$(".jf-modal-content").html(markup);
//addevents for validation inputs
self.addEventsToInputs();
//on load function after all the list rendered
self.settings.onLoad(docWindow.questionList, markup);
};
/**
* Serialize a given string to be compatible when pass a data to XHR
*/
this.serialize = function(data) {
var str = [];
for(var p in data)
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(data[p]));
return str.join("&");
};
/**
* Custom XHR request to communicate with the server
*/
this._xdr = function(url, method, data, callback, errback) {
var req;
if(XMLHttpRequest) {
req = new XMLHttpRequest();
if('withCredentials' in req) {
req.open(method, url, true);
req.setRequestHeader("Content-type","application/x-www-form-urlencoded");
req.onerror = errback;
req.onreadystatechange = function() {
if (req.readyState === 4) {
if (req.status >= 200 && req.status < 400) {
callback(req.responseText);
} else {
errback(req);
}
}
};
req.send(data);
}
} else if(XDomainRequest) {
req = new XDomainRequest();
req.open(method, url);
req.onerror = errback;
req.onload = function() {
callback(req.responseText);
};
req.send(data);
} else {
errback(req);
}
};
/**
* Automatically suggest a text which is suited for naming the Field
* It can take up the original text, strip spaces and convert to camelCase
*/
this.suggestText = function( string )
{
var uc_first = function(str) {
var f = str.charAt(0).toUpperCase();
return f + str.substr(1);
};
var max_suggested_len = 4;
//uppercase first character
string = uc_first(string);
//replace non alphanumeric characters with space
string = string.replace(/\W/g, ' ').replace(/\s+/g, ' ');
//if space is present, split it
if ( string.indexOf(' ') > -1 )
{
var a = string.split(' ')
, c = [];
for( var x = 0; x < a.length; x++ )
{
c.push( uc_first(a[x]) );
if ( (x+1) == max_suggested_len )
{
break;
}
}
string = c.join('');
}
return string;
};
/**
* Filter out questions that dont exactly be needed
*/
this.filterQuestionsType = function(questions)
{
var filtered = [];
//if ignore types is not undefined
if ( this.settings.ignore_types.length > 0 )
{
var ignored_types = this.settings.ignore_types;
for ( i in questions )
{
//if the questions type is not included to the ignored types, add it to the filtered array
if ( ignored_types.indexOf( questions[i].type ) < 0 )
{
filtered.push( questions[i] );
}
}
}
return (filtered.length > 0) ? filtered : questions;
};
/**
* Handles the pure validation of each element.
* responsible to display something to user
*/
this.validateInput = function(el)
{
var self = this
, icon = el.siblings('.fields-icon')
, id = el.attr('id');
//if the given regex to filter inputs not matched
//show error icon, otherwise okay
if ( !(self.settings.allowed_inputs.test(el.val())) ) {
icon.removeClass('okay').addClass('error').attr('title', 'You have an invalid input');
self.errorInputs[id] = true ;
} else {
icon.removeClass('error').addClass('okay').removeAttr('title');
delete self.errorInputs[id];
}
self.validateUnique(function(){
//check for errors
self.checkErrors();
});
};
/**
* Responsible on validating inputs
* to check wheter it was unique with the other inputs
*/
this.validateUnique = function(next)
{
var self = this;
if ( self.settings.unique === true )
{
var jq = self.widgetWindow.$
, field_list = jq(".jf-question-list input.fields")
, match_found = false;
var error = function(el)
{
el.siblings('.fields-icon')
.removeClass('okay')
.addClass('error')
.attr('title', 'You have an invalid input');
};
var rmerror = function(el)
{
el.siblings('.fields-icon')
.removeClass('error')
.addClass('okay')
.removeAttr('title');
};
//check if the given input is unique to the others
field_list.each(function(){
//dont include itself
var first = jq(this)
, first_id = first.attr('data-qid');
field_list.each(function(){
var second = jq(this)
, second_id = second.attr('data-qid');
//dont check itself
if ( first_id != second_id )
{
//there is a match, error them both
if ( first.val() == second.val() )
{
error(first);
self.uniqueErrorInputs[first_id] = second_id;
error(second);
}
else
{
//otherwise, make them clear
if (
typeof self.uniqueErrorInputs[first_id] !== 'undefined' &&
second_id == self.uniqueErrorInputs[first_id]
) {
rmerror(first);
delete self.uniqueErrorInputs[first_id];
rmerror(second);
}
// console.log("First", self.uniqueErrorInputs);
}
}
});
});
}
if (next) next();
};
/**
* Properly check errors, and return a callback containing a boolean value
* if error was occured or not - thus display the message
*/
this.checkErrors = function(cb)
{
var self = this
, jq = self.widgetWindow.$
, error = false;
var errorMessage = function(show,msg)
{
var el = jq('.jf-modal-footer > div:first-child')
, display = (show) ? 'inline-block' : 'none';
jq('.err_msg', el).text(msg);
el.css('display', display);
};
//remove any error messages
errorMessage(false);
//do what is necessary
if ( !self.isEmpty(self.errorInputs) )
{
errorMessage(true, self.settings.inputs_error_msg);
error = true;
}
//if unique is enabled, and there are errors
if ( self.settings.unique && !self.isEmpty(self.uniqueErrorInputs) )
{
errorMessage(true, self.settings.unique_error_msg);
error = true;
}
if (cb) cb(error);
};
/**
* Add single event to a single element
* on this, a blur event is added for validation
*/
this.addEventsToInputs = function()
{
var self = this
, jq = this.widgetWindow.$;
jq(".jf-question-list input.fields").on('blur', function(){
self.validateInput( jq(this) );
});
};
/**
* Check all the inputs if they have the proper values
* instant validation methd
*/
this.allInOneValidation = function(next)
{
var self = this
, jq = this.widgetWindow.$;
jq(".jf-question-list input.fields").each(function(){
self.validateInput( jq(this) );
}).promise().done(function(){
//check for errors
self.checkErrors(next);
});
};
/**
* Get the custom fields value from the input elements
* @param [function] next - callback if successfully executed the task
*/
this.getCustomFields = function(next)
{
var jq = this.widgetWindow.$
, customFieldsData = [];
if ( !jq || !jq("input.fields") ) return false;
var field_inputs = jq("input.fields", jq(this.widgetDocument.body));
if ( field_inputs.length > 0 )
{
field_inputs.each(function(index, el){
customFieldsData.push({
'qid': $(el).attr('data-qid'),
'value': $(el).val()
});
});
if (next) {
next(customFieldsData);
} else {
return customFieldsData;
}
}
};
/**
* Set the custom fields value to the element value
* @param [object] fields - the custom fields created from getCustomFields()
* @param [function] next - callback if successfully executed the task
*/
this.setCustomFields = function(cfields, next)
{
var questions = this.widgetWindow.questionList;
for( x in cfields )
{
//modify the question list
for( y in questions )
{
//if qid match, set the new object property
if ( questions[y].qid == cfields[x].qid )
{
questions[y]['modified_text'] = cfields[x].value;
}
}
}
//set the newly update question list
this.widgetWindow.questionList = questions;
if (next) next(questions);
};
/**
* Handles reading and writing of custom fields to database
* @param [string] type - {get} to read the custom fields and {save} to save the custom fields to database
* @param [function] next - callback if successfully executed the task
*/
this.handleCustomFields = function(type, next)
{
var self = this
, jq = self.widgetWindow.$;
var request_url = '//www.jotform.com/pickers/questionNaming/server.php'
, dataParam = {
formID: self.formID,
identifier: 'questionNaming'
};
if ( type == 'get' )
{
dataParam['action'] = 'getCustomField';
}
else if ( type == 'save' )
{
self.getCustomFields(function(custom_fields){
dataParam['action'] = 'saveCustomField';
dataParam['fieldsData'] = JSON.stringify(custom_fields);
self.setCustomFields(custom_fields);
});
}
self._xdr( request_url, 'post', self.serialize(dataParam), function(response) {
if ( type == 'save' ) {
if (next) next(self.widgetWindow.questionList);
} else if ( type == 'get' ) {
if (next) next(response);
}
}, function error(e){
console.error("Error occured!!", e);
self.stopLoading(self.widgetDocument, "Something went wrong loading previous entered data. Reload picker or Click here to load defaults.");
jq('#continueStopped', jq(self.widgetDocument.body)).click(function(e){
if (next) next(false);
return false;
});
});
};
/**
* Fetch custom fields that was saved to the server
* - any previous input by user will automatically be loaded
* @param [function] next - the callback when successfully fetched the data
*/
this.fetchCustomFields = function(next)
{
var self = this;
this.handleCustomFields('get', function(f){
var cfields = false;
//if something goes wrong, continue loading defaults
if (f != false) {
cfields = JSON.parse(f).result;
}
//set the new value of questionList
self.setCustomFields(cfields);
if (next) next();
});
};
this.getIframeDocument = function(iframe)
{
var doc;
if(iframe.contentDocument)
// Firefox, Opera
doc = iframe.contentDocument;
else if(iframe.contentWindow)
// Internet Explorer
doc = iframe.contentWindow.document;
else if(iframe.document)
// Others?
doc = iframe.document;
if(doc == null)
throw "Document not initialized";
return doc;
};
this.getIframeWindow = function(iframe_ob)
{
var doc;
if (iframe_ob.contentWindow) {
return iframe_ob.contentWindow;
}
if (iframe_ob.window) {
return iframe_ob.window;
}
if (!doc && iframe_ob.contentDocument) {
doc = iframe_ob.contentDocument;
}
if (!doc && iframe_ob.document) {
doc = iframe_ob.document;
}
if (doc && doc.defaultView) {
return doc.defaultView;
}
if (doc && doc.parentWindow) {
return doc.parentWindow;
}
return undefined;
};
this.removeElement = function(EId)
{
return(EObj=document.getElementById(EId)) ? EObj.parentNode.removeChild(EObj) : false;
};
this.buildModalBox = function(options)
{
var self = this
, container = document.body
, dimmer = document.createElement('div')
, frame = document.createElement('iframe');
frame.className = 'centered';
frame.setAttribute('id', 'jotform-window');
dimmer.setAttribute('id', 'jotform_modal');
dimmer.className = 'jf-mask';
dimmer.style.display = 'none';
dimmer.appendChild(frame);
container.appendChild(dimmer);
var doc = this.widgetDocument = this.getIframeDocument(frame)
, iWindow = this.widgetWindow = this.getIframeWindow(frame);
//necessary for FF & IE
doc.open();
doc.close();
//rest of the elements are created in iframe
var wrapperDiv = doc.createElement('div')
, modalHeader = doc.createElement('div')
, modalTitle = doc.createElement('div')
, modalTitleInner = doc.createElement('h1')
, modalContent = doc.createElement('div')
, closeButton = doc.createElement('div')
, modalFooter = doc.createElement('div')
, submitButton = doc.createElement('button');
wrapperDiv.className = 'jf-modal-window';
modalHeader.className = 'jf-modal-header';
modalTitle.className = 'jf-modal-title';
modalContent.className = 'jf-modal-content';
//append titlename to title container and append them header
modalTitleInner.className = 'jf-modal-title-inner';
modalTitleInner.innerHTML = self.settings.title;
modalTitle.appendChild(modalTitleInner);
modalHeader.appendChild(modalTitle);
//close button and append to header
closeButton.className = 'jf-modal-close';
closeButton.innerHTML = '✖';
modalHeader.appendChild(closeButton);
modalFooter.className = 'jf-modal-footer';
submitButton.className = 'jotform-modal-submit';
//innerText is not applicable for FF & IE
submitButton.innerHTML = 'Continue';
//deploy closeModal function
var closeModal = function() {
self.removeElement('jotform_modal');
options.onClose();
};
//submit button onclick event
submitButton.onclick = function() {
self.allInOneValidation(function(errored){
if ( errored )
{
return false;
}
var finalize = function(questions)
{
self.settings.onSubmit(questions);
self.removeElement('jotform_modal');
};
if ( self.settings.remember )
{
self.handleCustomFields('save', function(questions){
finalize(questions);
});
}
else
{
var cfields = self.getCustomFields();
self.setCustomFields(cfields, function(questions){
finalize(questions);
});
}
});
};
//create the loading container
var l_state = doc.createElement('div')
, l_text = doc.createElement('div');
l_text.setAttribute('id', 'jf-loading-text');
l_state.className = 'jf-loading-state';
l_state.appendChild(l_text);
modalContent.innerHTML = l_state.outerHTML;
//add error message element
var err_div = doc.createElement('div')
, err_span_icon = doc.createElement('span')
, err_span_msg = doc.createElement('span');
err_div.style.display = 'none';
err_span_icon.className = 'err_msg_icon';
err_span_msg.className = 'err_msg';
err_div.appendChild(err_span_icon);
err_div.appendChild(err_span_msg);
//Add child elements
modalFooter.appendChild(err_div);
modalFooter.appendChild(submitButton);
wrapperDiv.appendChild(modalHeader);
wrapperDiv.appendChild(modalContent);
wrapperDiv.appendChild(modalFooter);
iWindow.addEventListener('message',function(event) {
if(event.data.match(/setHeight/) === null) {
options.onMessage.call(options.scope, event.data);
}
}, false);
//append everything to iframe doc body
doc.body.appendChild(wrapperDiv);
//close modal once close button is click
closeButton.onclick = function() {
closeModal();
};
//load the entire modal css
self.loadCSS(doc, self.commonCSS, function() {
self.loadCSS(doc, self.modalCSS, function() {
//show modal
dimmer.style.display = 'block';
//call render function together with ready
options.onRender(self.widgetWindow, self.widgetDocument);
options.onReady();
});
});
};
this.createContent = function(settings)
{
var self = this;
this.buildModalBox({
scope : self,
onRender : function(iWindow, iDoc) {
//show some message
self.showMessage(iDoc, "Loading Questions, please wait...");
//pass widget options
iWindow.widgetOptions = settings;
var f = document.getElementById('jotform-window');
//pass initial form reports instead of loading it
//useful if you check a form has forms and use the data pulled later
if ( settings.initial_data !== false ) {
iWindow.questionList = settings.initial_data;
self.loadJS(iDoc, iWindow);
} else {
base.getFormQuestions(self.formID, function(resp) {
var questionsArr = self.toArray(resp);
iWindow.questionList = self.filterQuestionsType(questionsArr);
if ( iWindow.questionList.length == 0 ) {
var errmsg = "No Questions available, it seems that your Form is empty.";
self.stopLoading(iDoc, errmsg);
} else {
self.loadJS(iDoc, iWindow);
}
}, function error(){
var errmsg = "Something went wrong when fetching questions, please try again.";
self.stopLoading(iDoc, errmsg);
throw errmsg;
});
}
},
onReady : settings.onReady,
onClose : settings.onClose
});
};
this.showMessage = function(iDoc, str, classname)
{
var loading = iDoc.querySelector('.jf-loading-state #jf-loading-text');
loading.innerHTML = str;
if ( typeof classname !== 'undefined' ) {
loading.className = classname;
}
};
this.stopLoading = function(iDoc, errmsg)
{
this.showMessage(iDoc, errmsg, "jf-loading-stop");
};
this.isEmpty = function(obj)
{
for(var prop in obj) {
if(obj.hasOwnProperty(prop))
return false;
}
return true;
};
this.toArray = function(obj)
{
var arr =[];
for( var i in obj ) {
if (obj.hasOwnProperty(i)){
arr.push(obj[i]);
}
}
return arr;
};
this.extend = function()
{
var a = arguments[0];
for (var i = 1, len = arguments.length; i < len; i++) {
var b = arguments[i];
for (var prop in b) {
a[prop] = b[prop];
}
}
return a;
};
this.init = function(formID, options)
{
this.formID = formID;
this.settings = this.extend({}, this.settings, options);
this.loadCSS(document, this.overlayCSS, function(){});
this.createContent(this.settings);
};
};
base.QuestionNaming = function(formID, options)
{
if ( !formID || formID.length == 0 || typeof formID === 'undefined' ) throw "Form ID is missing";
var _q_naming = new QuestionNaming();
_q_naming.init(String(formID), options);
};
})(JF || {});