This commit is contained in:
David Baldwynn 2016-06-07 14:22:20 -07:00
commit a8530472d5
48 changed files with 1467 additions and 412 deletions

View file

@ -19,12 +19,12 @@ var mongoose = require('mongoose'),
*/ */
exports.uploadPDF = function(req, res, next) { exports.uploadPDF = function(req, res, next) {
console.log('inside uploadPDF'); //console.log('inside uploadPDF');
// console.log('\n\nProperty Descriptor\n-----------'); // console.log('\n\nProperty Descriptor\n-----------');
// console.log(Object.getOwnPropertyDescriptor(req.files.file, 'path')); // console.log(Object.getOwnPropertyDescriptor(req.files.file, 'path'));
console.log(req.file); //console.log(req.file);
if(req.file){ if(req.file){
var pdfFile = req.file; var pdfFile = req.file;
@ -159,9 +159,20 @@ exports.deleteSubmissions = function(req, res) {
res.status(400).send({ res.status(400).send({
message: errorHandler.getErrorMessage(err) message: errorHandler.getErrorMessage(err)
}); });
return;
} }
res.status(200).send('Form submissions successfully deleted'); form.analytics.visitors = [];
form.save(function(err){
if(err){
res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
return;
}
res.status(200).send('Form submissions successfully deleted');
});
}); });
}; };
@ -171,8 +182,6 @@ exports.deleteSubmissions = function(req, res) {
exports.createSubmission = function(req, res) { exports.createSubmission = function(req, res) {
var form = req.form; var form = req.form;
// console.log('in createSubmission()');
// console.log(req.body);
var submission = new FormSubmission({ var submission = new FormSubmission({
admin: req.form.admin._id, admin: req.form.admin._id,
@ -210,10 +219,9 @@ exports.createSubmission = function(req, res) {
} }
submission.save(function(err, submission){ submission.save(function(err, submission){
// console.log('in submissions.save()\n submission: '+JSON.stringify(submission) )
if(err){ if(err){
console.log(err.message); console.log(err.message);
res.status(400).send({ res.status(500).send({
message: errorHandler.getErrorMessage(err) message: errorHandler.getErrorMessage(err)
}); });
} }
@ -281,8 +289,9 @@ exports.create = function(req, res) {
exports.read = function(req, res) { exports.read = function(req, res) {
var validUpdateTypes= Form.schema.path('plugins.oscarhost.settings.updateType').enumValues; var validUpdateTypes= Form.schema.path('plugins.oscarhost.settings.updateType').enumValues;
var newForm = JSON.parse(JSON.stringify(req.form)); var newForm = req.form.toJSON({virtuals : true});
newForm.plugins.oscarhost.settings.validUpdateTypes = validUpdateTypes; newForm.plugins.oscarhost.settings.validUpdateTypes = validUpdateTypes;
res.json(newForm); res.json(newForm);
}; };
@ -377,11 +386,12 @@ exports.formByID = function(req, res, next, id) {
} }
else { else {
//Remove sensitive information from User object //Remove sensitive information from User object
form.admin.password = undefined; var _form = form;
form.admin.salt = undefined; _form.admin.password = undefined;
form.provider = undefined; _form.admin.salt = undefined;
_form.provider = undefined;
req.form = form; req.form = _form;
return next(); return next();
} }
}); });

View file

@ -46,6 +46,21 @@ var ButtonSchema = new Schema({
} }
}); });
var VisitorDataSchema = new Schema({
referrer: {
type: String
},
lastActiveField: {
type: Schema.Types.ObjectId
},
timeElapsed: {
type: Number
},
isSubmitted: {
type: Boolean
}
});
/** /**
* Form Schema * Form Schema
*/ */
@ -65,10 +80,15 @@ var FormSchema = new Schema({
type: String, type: String,
default: '' default: ''
}, },
form_fields: {
type: [FieldSchema] analytics:{
gaCode: {
type: String
},
visitors: [VisitorDataSchema]
}, },
form_fields: [FieldSchema],
submissions: [{ submissions: [{
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'FormSubmission' ref: 'FormSubmission'
@ -195,16 +215,91 @@ var FormSchema = new Schema({
}, },
auth: { auth: {
user: { user: {
type: String, type: String
}, },
pass: { pass: {
type: String, type: String
} }
} }
} }
} }
}); });
/*
** In-Form Analytics Virtual Attributes
*/
FormSchema.virtual('analytics.views').get(function () {
return this.analytics.visitors.length;
});
FormSchema.virtual('analytics.submissions').get(function () {
return this.submissions.length;
});
FormSchema.virtual('analytics.conversionRate').get(function () {
return this.submissions.length/this.analytics.visitors.length*100;
});
FormSchema.virtual('analytics.fields').get(function () {
var fieldDropoffs = [];
var visitors = this.analytics.visitors;
var that = this;
for(var i=0; i<this.form_fields.length; i++){
var field = this.form_fields[i];
if(!field.deletePreserved){
var dropoffViews = _.reduce(visitors, function(sum, visitorObj){
if(visitorObj.lastActiveField+'' === field._id+'' && !visitorObj.isSubmitted){
return sum + 1;
}
return sum;
}, 0);
var continueViews, nextIndex;
if(i !== this.form_fields.length-1){
continueViews = _.reduce(visitors, function(sum, visitorObj){
nextIndex = that.form_fields.indexOf(_.find(that.form_fields, function(o) {
return o._id+'' === visitorObj.lastActiveField+'';
}));
if(nextIndex > i){
return sum + 1;
}
return sum;
}, 0);
}else {
continueViews = _.reduce(visitors, function(sum, visitorObj){
if(visitorObj.lastActiveField+'' === field._id+'' && visitorObj.isSubmitted){
return sum + 1;
}
return sum;
}, 0);
}
var totalViews = dropoffViews+continueViews;
var continueRate = continueViews/totalViews*100;
var dropoffRate = dropoffViews/totalViews*100;
fieldDropoffs[i] = {
dropoffViews: dropoffViews,
continueViews: continueViews,
totalViews: totalViews,
continueRate: continueRate,
dropoffRate: dropoffRate,
field: field
};
}
}
return fieldDropoffs;
});
FormSchema.plugin(mUtilities.timestamp, { FormSchema.plugin(mUtilities.timestamp, {
createdPath: 'created', createdPath: 'created',
modifiedPath: 'lastModified', modifiedPath: 'lastModified',
@ -397,7 +492,7 @@ FormSchema.pre('save', function (next) {
//Find FormSubmissions that contain field with _id equal to 'deleted_id' //Find FormSubmissions that contain field with _id equal to 'deleted_id'
FormSubmission. FormSubmission.
find({ form: that._id, admin: that.admin, form_fields: {$elemMatch: {_id: deleted_id} } }). find({ form: that._id, admin: that.admin, form_fields: {$elemMatch: {submissionId: deleted_id} } }).
exec(function(err, submissions){ exec(function(err, submissions){
if(err) { if(err) {
console.error(err); console.error(err);

View file

@ -4,6 +4,7 @@
* Module dependencies. * Module dependencies.
*/ */
var mongoose = require('mongoose'), var mongoose = require('mongoose'),
util = require('util'),
mUtilities = require('mongoose-utilities'), mUtilities = require('mongoose-utilities'),
_ = require('lodash'), _ = require('lodash'),
Schema = mongoose.Schema; Schema = mongoose.Schema;
@ -23,85 +24,188 @@ var FieldOptionSchema = new Schema({
} }
}); });
var RatingFieldSchema = new Schema({
steps: {
type: Number,
min: 1,
max: 10
},
shape: {
type: String,
enum: [
'Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'
]
},
validShapes: {
type: [String]
}
});
/** /**
* FormField Schema * FormField Schema
*/ */
var FormFieldSchema = new Schema({ function BaseFieldSchema(){
title: { Schema.apply(this, arguments);
type: String,
trim: true,
required: 'Field Title cannot be blank'
},
description: {
type: String,
default: ''
},
logicJump: { this.add({
type: Schema.Types.ObjectId, isSubmission: {
ref: 'LogicJump' type: Boolean,
}, default: false
},
submissionId: {
type: Schema.Types.ObjectId
},
title: {
type: String,
trim: true,
required: 'Field Title cannot be blank'
},
description: {
type: String,
default: ''
},
fieldOptions: [FieldOptionSchema], logicJump: {
required: { type: Schema.Types.ObjectId,
type: Boolean, ref: 'LogicJump'
default: true },
},
disabled: {
type: Boolean,
default: false
},
deletePreserved: { ratingOptions: {
type: Boolean, type: RatingFieldSchema,
default: false required: false,
}, default: {}
validFieldTypes: { },
type: [String] fieldOptions: [FieldOptionSchema],
}, required: {
fieldType: { type: Boolean,
type: String, default: true
required: true, },
enum: [ disabled: {
'textfield', type: Boolean,
'date', default: false
'email', },
'link',
'legal', deletePreserved: {
'url', type: Boolean,
'textarea', default: false
'statement', },
'welcome', validFieldTypes: {
'thankyou', type: [String]
'file', },
'dropdown', fieldType: {
'scale', type: String,
'rating', required: true,
'radio', enum: [
'checkbox', 'textfield',
'hidden', 'date',
'yes_no', 'email',
'natural', 'link',
'number' 'legal',
] 'url',
}, 'textarea',
fieldValue: Schema.Types.Mixed 'statement',
'welcome',
'thankyou',
'file',
'dropdown',
'scale',
'rating',
'radio',
'checkbox',
'hidden',
'yes_no',
'natural',
'number'
]
},
fieldValue: Schema.Types.Mixed
});
this.plugin(mUtilities.timestamp, {
createdPath: 'created',
modifiedPath: 'lastModified',
useVirtual: false
});
this.pre('save', function (next) {
this.validFieldTypes = mongoose.model('Field').schema.path('fieldType').enumValues;
if(this.fieldType === 'rating' && this.ratingOptions.validShapes.length === 0){
this.ratingOptions.validShapes = mongoose.model('RatingOptions').schema.path('shape').enumValues;
}
next();
});
}
util.inherits(BaseFieldSchema, Schema);
var FormFieldSchema = new BaseFieldSchema();
FormFieldSchema.pre('validate', function(next) {
var error = new mongoose.Error.ValidationError(this);
//If field is rating check that it has ratingOptions
if(this.fieldType !== 'rating'){
if(this.ratingOptions && this.ratingOptions.steps && this.ratingOptions.shape){
error.errors.ratingOptions = new mongoose.Error.ValidatorError({path: 'ratingOptions', message: 'ratingOptions is only allowed for type \'rating\' fields.', type: 'notvalid', value: this.ratingOptions});
return(next(error));
}
}else{
//Setting default values for ratingOptions
if(!this.ratingOptions.steps){
this.ratingOptions.steps = 10;
}
if(!this.ratingOptions.shape){
this.ratingOptions.shape = 'Star';
}
//Checking that the fieldValue is between 0 and ratingOptions.steps
if(this.fieldValue+0 > this.ratingOptions.steps || this.fieldValue+0 < 0){
this.fieldValue = 1;
}
}
//If field is multiple choice check that it has field
if(this.fieldType !== 'dropdown' && this.fieldType !== 'radio' && this.fieldType !== 'checkbox'){
if(!this.fieldOptions || this.fieldOptions.length !== 0){
error.errors.ratingOptions = new mongoose.Error.ValidatorError({path:'fieldOptions', message: 'fieldOptions are only allowed for type dropdown, checkbox or radio fields.', type: 'notvalid', value: this.ratingOptions});
return(next(error));
}
}
return next();
}); });
FormFieldSchema.plugin(mUtilities.timestamp, { //Submission fieldValue correction
createdPath: 'created', FormFieldSchema.pre('save', function(next) {
modifiedPath: 'lastModified',
useVirtual: false
});
FormFieldSchema.pre('save', function (next){ if(this.fieldType === 'dropdown' && this.isSubmission){
this.validFieldTypes = mongoose.model('Field').schema.path('fieldType').enumValues; //console.log(this);
next(); this.fieldValue = this.fieldValue.option_value;
//console.log(this.fieldValue);
}
return next();
}); });
mongoose.model('Field', FormFieldSchema); var Field = mongoose.model('Field', FormFieldSchema);
var RatingOptions = mongoose.model('RatingOptions', RatingFieldSchema);
module.exports = FormFieldSchema; module.exports = FormFieldSchema;

View file

@ -17,6 +17,8 @@ var mongoose = require('mongoose'),
FieldSchema = require('./form_field.server.model.js'), FieldSchema = require('./form_field.server.model.js'),
OscarSecurity = require('../../scripts/oscarhost/OscarSecurity'); OscarSecurity = require('../../scripts/oscarhost/OscarSecurity');
var FieldSchema = require('./form_field.server.model.js');
var newDemoTemplate = { var newDemoTemplate = {
address: '880-9650 Velit. St.', address: '880-9650 Velit. St.',
city: '', city: '',
@ -41,6 +43,18 @@ var newDemoTemplate = {
yearOfBirth: '2015' yearOfBirth: '2015'
}; };
// Setter function for form_fields
function formFieldsSetter(form_fields){
for(var i=0; i<form_fields.length; i++){
form_fields[i].isSubmission = true;
form_fields[i].submissionId = form_fields[i]._id;
form_fields[i]._id = new mongoose.mongo.ObjectID();
}
//console.log(form_fields)
return form_fields;
}
/** /**
* Form Submission Schema * Form Submission Schema
*/ */
@ -55,9 +69,7 @@ var FormSubmissionSchema = new Schema({
required: true required: true
}, },
form_fields: { form_fields: [FieldSchema],
type: [Schema.Types.Mixed]
},
form: { form: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
@ -99,7 +111,7 @@ var FormSubmissionSchema = new Schema({
}, },
timeElapsed: { timeElapsed: {
type: Number, type: Number
}, },
percentageComplete: { percentageComplete: {
type: Number type: Number
@ -116,9 +128,10 @@ var FormSubmissionSchema = new Schema({
default: false default: false
} }
} }
}); });
FormSubmissionSchema.path('form_fields').set(formFieldsSetter);
FormSubmissionSchema.plugin(mUtilities.timestamp, { FormSubmissionSchema.plugin(mUtilities.timestamp, {
createdPath: 'created', createdPath: 'created',
modifiedPath: 'lastModified', modifiedPath: 'lastModified',
@ -214,7 +227,7 @@ FormSubmissionSchema.pre('save', function (next) {
}); });
}); });
} }
}, }
], function(err, result) { ], function(err, result) {
if(err) return next(err); if(err) return next(err);
@ -254,7 +267,6 @@ FormSubmissionSchema.pre('save', function (next) {
self = this, self = this,
_form = this.form; _form = this.form;
if(this.pdf && this.pdf.path){ if(this.pdf && this.pdf.path){
dest_filename = self.title.replace(/ /g,'')+'_submission_'+Date.now()+'.pdf'; dest_filename = self.title.replace(/ /g,'')+'_submission_'+Date.now()+'.pdf';
var __path = this.pdf.path.split('/').slice(0,this.pdf.path.split('/').length-1).join('/'); var __path = this.pdf.path.split('/').slice(0,this.pdf.path.split('/').length-1).join('/');

View file

@ -0,0 +1,80 @@
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
config = require('../../config/config'),
errorHandler = require('../controllers/errors.server.controller'),
Form = mongoose.model('Form');
// Create the chat configuration
module.exports = function (io, socket) {
var visitorsData = {};
var saveVisitorData = function (data, cb){
Form.findById(data.formId, function(err, form) {
if (err) {
console.log(err);
throw new Error(errorHandler.getErrorMessage(err));
}
var newVisitor = {
referrer: data.referrer,
lastActiveField: data.lastActiveField,
timeElapsed: data.timeElapsed,
isSubmitted: data.isSubmitted
};
form.analytics.visitors.push(newVisitor);
form.save(function (err) {
if (err) {
console.log(err);
throw new Error(errorHandler.getErrorMessage(err));
}
console.log('\n\nVisitor data successfully added!');
delete visitorsData[socket.id];
if(cb) cb();
});
});
socket.disconnect(0);
};
io.on('connection', function(socket) {
// a user has visited our page - add them to the visitorsData object
socket.on('form-visitor-data', function(data) {
console.log('\n\nuser has visited our page');
visitorsData[socket.id] = data;
console.log(data);
if (data.isSubmitted) {
saveVisitorData(data, function () {
console.log('\n\n user submitted form');
socket.disconnect(0);
});
}
});
socket.on('disconnect', function() {
var data = visitorsData[socket.id];
if(data){
if(!data.isSubmitted) {
saveVisitorData(data);
}
}
});
});
};

View file

@ -80,8 +80,12 @@
<!--Embedding The signupDisabled Boolean--> <!--Embedding The signupDisabled Boolean-->
<script type="text/javascript"> <script type="text/javascript">
var signupDisabled = {{signupDisabled | safe}}; var signupDisabled = {{signupDisabled | safe}};
var socketPort = {{socketPort | safe}};
</script> </script>
<!--Socket.io Client Dependency-->
<script src="https://cdn.socket.io/socket.io-1.4.5.js"></script>
<!--Bower JS dependencies--> <!--Bower JS dependencies-->
{% for bowerJSFile in bowerJSFiles %} {% for bowerJSFile in bowerJSFiles %}
<script type="text/javascript" src="{{bowerJSFile}}"></script> <script type="text/javascript" src="{{bowerJSFile}}"></script>
@ -103,7 +107,7 @@
<script src="https://cdn.ravenjs.com/2.3.0/angular/raven.min.js"></script> <script src="https://cdn.ravenjs.com/2.3.0/angular/raven.min.js"></script>
<script> <script>
Raven.config('http://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install(); Raven.config('https://825fefd6b4ed4a4da199c1b832ca845c@sentry.tellform.com/2').install();
</script> </script>
<!-- [if lt IE 9]> <!-- [if lt IE 9]>

View file

@ -27,7 +27,6 @@
"angular-bootstrap-colorpicker": "~3.0.19", "angular-bootstrap-colorpicker": "~3.0.19",
"angular-ui-router-tabs": "~1.7.0", "angular-ui-router-tabs": "~1.7.0",
"angular-scroll": "^1.0.0", "angular-scroll": "^1.0.0",
"ui-select": "angular-ui-select#^0.16.1",
"angular-sanitize": "^1.5.3", "angular-sanitize": "^1.5.3",
"v-button": "^1.1.1", "v-button": "^1.1.1",
"angular-busy": "^4.1.3", "angular-busy": "^4.1.3",
@ -35,10 +34,13 @@
"raven-js": "^3.0.4", "raven-js": "^3.0.4",
"tableExport.jquery.plugin": "^1.5.1", "tableExport.jquery.plugin": "^1.5.1",
"angular-translate": "~2.11.0" "angular-translate": "~2.11.0"
"js-yaml": "^3.6.1",
"angular-ui-select": "whitef0x0/ui-select#compiled"
}, },
"resolutions": { "resolutions": {
"angular-bootstrap": "^0.14.0", "angular-bootstrap": "^0.14.0",
"angular": "1.4.x" "angular": "~1.4.7",
"angular-ui-select": "compiled"
}, },
"overrides": { "overrides": {
"BOWER-PACKAGE": { "BOWER-PACKAGE": {

View file

@ -12,6 +12,7 @@ var _ = require('lodash'),
var exists = require('path-exists').sync; var exists = require('path-exists').sync;
var minBowerFiles = function(type){ var minBowerFiles = function(type){
console.log(type);
return bowerFiles(type).map( function(path, index, arr) { return bowerFiles(type).map( function(path, index, arr) {
var newPath = path.replace(/.([^.]+)$/g, '.min.$1'); var newPath = path.replace(/.([^.]+)$/g, '.min.$1');
return exists( newPath ) ? newPath : path; return exists( newPath ) ? newPath : path;

36
config/env/all.js vendored
View file

@ -7,7 +7,9 @@ module.exports = {
description: process.env.APP_DESC || 'Opensource form builder alternative to TypeForm', description: process.env.APP_DESC || 'Opensource form builder alternative to TypeForm',
keywords: process.env.APP_KEYWORDS || 'typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs' keywords: process.env.APP_KEYWORDS || 'typeform, pdfs, forms, opensource, formbuilder, google forms, nodejs'
}, },
port: process.env.PORT || 3000, port: process.env.PORT || 5000,
socketPort: process.env.SOCKET_PORT || 35729,
templateEngine: 'swig', templateEngine: 'swig',
reCAPTCHA_Key: process.env.reCAPTCHA_KEY || '', reCAPTCHA_Key: process.env.reCAPTCHA_KEY || '',
@ -70,9 +72,9 @@ module.exports = {
assets: { assets: {
css: [ css: [
'public/modules/**/css/*.css', 'public/modules/**/css/*.css',
'!public/modules/**/demo/**/*.css', 'public/modules/**/demo/**/*.css',
'!public/modules/**/dist/**/*.css', 'public/modules/**/dist/**/*.css',
'!public/modules/**/node_modules/**/*.css' 'public/modules/**/node_modules/**/*.css'
], ],
js: [ js: [
'public/dist/populate_template_cache.js', 'public/dist/populate_template_cache.js',
@ -82,29 +84,29 @@ module.exports = {
'public/modules/*/*.js', 'public/modules/*/*.js',
'public/modules/*/*/*.js', 'public/modules/*/*/*.js',
'public/modules/**/*.js', 'public/modules/**/*.js',
'!public/modules/**/gruntfile.js', 'public/modules/**/gruntfile.js',
'!public/modules/**/demo/**/*.js', 'public/modules/**/demo/**/*.js',
'!public/modules/**/dist/**/*.js', 'public/modules/**/dist/**/*.js',
'!public/modules/**/node_modules/**/*.js', 'public/modules/**/node_modules/**/*.js',
'!public/modules/**/tests/**/*.js' 'public/modules/**/tests/**/*.js'
], ],
views: [ views: [
'public/modules/**/*.html', 'public/modules/**/*.html',
'!public/modules/**/demo/**/*.html', 'public/modules/**/demo/**/*.html',
'!public/modules/**/dist/**/*.html', 'public/modules/**/dist/**/*.html',
'!public/modules/**/node_modules/**/*.html', 'public/modules/**/node_modules/**/*.html',
'!public/modules/**/tests/**/*.html' 'public/modules/**/tests/**/*.html'
], ],
unit_tests: [ unit_tests: [
'public/lib/angular-mocks/angular-mocks.js', 'public/lib/angular-mocks/angular-mocks.js',
'public/modules/*/tests/unit/**/*.js', 'public/modules/*/tests/unit/**/*.js',
'!public/modules/**/demo/**/*.js', 'public/modules/**/demo/**/*.js',
'!public/modules/**/node_modules/**/*.js' 'public/modules/**/node_modules/**/*.js'
], ],
e2e_tests: [ e2e_tests: [
'public/modules/*/tests/e2e/**.js', 'public/modules/*/tests/e2e/**.js',
'!public/modules/**/demo/**/*.js', 'public/modules/**/demo/**/*.js',
'!public/modules/**/node_modules/**/*.js' 'public/modules/**/node_modules/**/*.js'
] ]
} }
}; };

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
module.exports = { module.exports = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000', baseUrl: process.env.BASE_URL || 'http://localhost:5000',
db: { db: {
uri: 'mongodb://'+(process.env.DB_HOST || 'localhost')+'/mean', uri: 'mongodb://'+(process.env.DB_HOST || 'localhost')+'/mean',
options: { options: {

17
config/env/local.js vendored
View file

@ -1,17 +0,0 @@
'use strict';
var mandrill_api_key = 'AVCCf1C2dFlrNhx9Iyi_yQ';
//Use mandrill mock API_key if we are testing
// if(process.env.NODE_ENV === 'test') mandrill_api_key = '_YNOKLgT9DGb2sgVGR66yQ';
module.exports = {
// db: {
// uri: 'mongodb://localhost/local-dev',
// options: {
// user: '',
// pass: ''
// }
// },
sessionSecret: process.env.SESSION_SECRET || 'somethingheresecret',
};

View file

@ -1,65 +0,0 @@
'use strict';
module.exports = {
baseUrl: process.env.BASE_URL || 'dev.tellform.com',
db: {
uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/mean',
options: {
user: 'admin',
pass: process.env.MONGOLAB_PASS || 'admin'
}
},
log: {
// Can specify one of 'combined', 'common', 'dev', 'short', 'tiny'
format: 'dev',
// Stream defaults to process.stdout
// Uncomment to enable logging to a log on the file system
options: {
stream: 'access.log'
}
},
sessionCookie: {
domain: process.env.BASE_URL || 'dev.tellform.com'
},
assets: {
css: 'public/dist/application.min.css',
js: 'public/dist/application.min.js'
},
facebook: {
clientID: process.env.FACEBOOK_ID || 'APP_ID',
clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET',
callbackURL: '/auth/facebook/callback'
},
twitter: {
clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY',
clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET',
callbackURL: '/auth/twitter/callback'
},
google: {
clientID: process.env.GOOGLE_ID || 'APP_ID',
clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET',
callbackURL: '/auth/google/callback'
},
linkedin: {
clientID: process.env.LINKEDIN_ID || 'APP_ID',
clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET',
callbackURL: '/auth/linkedin/callback'
},
github: {
clientID: process.env.GITHUB_ID || 'APP_ID',
clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET',
callbackURL: '/auth/github/callback'
},
mailer: {
from: process.env.MAILER_FROM || 'no-reply@dev.tellform.com',
options: {
service: process.env.MAILER_SERVICE_PROVIDER || '',
secure: false,
requireTLS: true,
auth: {
user: process.env.MAILER_EMAIL_ID || '',
pass: process.env.MAILER_PASSWORD || ''
}
}
}
};

View file

@ -26,7 +26,16 @@ var fs = require('fs-extra'),
device = require('express-device'), device = require('express-device'),
client = new raven.Client(config.DSN); client = new raven.Client(config.DSN);
/**
* Configure Socket.io
*/
var configureSocketIO = function (app, db) {
// Load the Socket.io configuration
var server = require('./socket.io')(app, db);
// Return server object
return server;
};
module.exports = function(db) { module.exports = function(db) {
// Initialize express app // Initialize express app
@ -43,6 +52,7 @@ module.exports = function(db) {
app.locals.signupDisabled = config.signupDisabled; app.locals.signupDisabled = config.signupDisabled;
app.locals.description = config.app.description; app.locals.description = config.app.description;
app.locals.keywords = config.app.keywords; app.locals.keywords = config.app.keywords;
app.locals.socketPort = config.socketPort;
app.locals.bowerJSFiles = config.getBowerJSAssets(); app.locals.bowerJSFiles = config.getBowerJSAssets();
app.locals.bowerCssFiles = config.getBowerCSSAssets(); app.locals.bowerCssFiles = config.getBowerCSSAssets();
@ -147,11 +157,11 @@ module.exports = function(db) {
// Add headers for Sentry // Add headers for Sentry
/*
app.use(function (req, res, next) { app.use(function (req, res, next) {
// Website you wish to allow to connect // Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', 'http://sentry.polydaic.com'); res.setHeader('Access-Control-Allow-Origin', 'https://sentry.polydaic.com');
// Request methods you wish to allow // Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
@ -166,7 +176,7 @@ module.exports = function(db) {
// Pass to next layer of middleware // Pass to next layer of middleware
next(); next();
}); });
*/
// Sentry (Raven) middleware // Sentry (Raven) middleware
// app.use(raven.middleware.express.requestHandler(config.DSN)); // app.use(raven.middleware.express.requestHandler(config.DSN));
@ -212,6 +222,8 @@ module.exports = function(db) {
return httpsServer; return httpsServer;
} }
app = configureSocketIO(app, db);
// Return Express server instance // Return Express server instance
return app; return app;
}; };

24
config/socket.io.js Normal file
View file

@ -0,0 +1,24 @@
'use strict';
// Load the module dependencies
var config = require('./config'),
path = require('path'),
http = require('http'),
socketio = require('socket.io');
// Define the Socket.io configuration method
module.exports = function (app, db) {
var server = http.createServer(app);
// Create a new Socket.io server
var io = socketio.listen(server);
// Add an event listener to the 'connection' event
io.on('connection', function (socket) {
config.getGlobbedFiles('./app/sockets/**.js').forEach(function (socketConfiguration) {
require(path.resolve(socketConfiguration))(io, socket);
});
});
return server;
};

View file

@ -0,0 +1,12 @@
'use strict';
/**
* Module dependencies.
*/
var passport = require('passport'),
AnonymousStrategy = require('passport-anonymous').Strategy;
module.exports = function() {
// Use local strategy
passport.use(new AnonymousStrategy());
};

View file

@ -19,7 +19,7 @@
"scripts": { "scripts": {
"start": "grunt", "start": "grunt",
"test": "grunt test && grunt coveralls", "test": "grunt test && grunt coveralls",
"postinstall": "bower install --config.interactive=false; grunt build" "postinstall": "bower install --config.interactive=false; grunt build; node scripts/setup.js;"
}, },
"dependencies": { "dependencies": {
"async": "^1.4.2", "async": "^1.4.2",
@ -63,13 +63,14 @@
"math": "0.0.3", "math": "0.0.3",
"method-override": "~2.3.0", "method-override": "~2.3.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mongoose": "3.8.40", "mongoose": "~4.4.19",
"mongoose-utilities": "~0.1.1", "mongoose-utilities": "~0.1.1",
"morgan": "~1.6.1", "morgan": "~1.6.1",
"multer": "~1.1.0", "multer": "~1.1.0",
"node-freegeoip": "0.0.1", "node-freegeoip": "0.0.1",
"nodemailer": "~1.10.0", "nodemailer": "~1.10.0",
"passport": "~0.3.0", "passport": "~0.3.0",
"passport-anonymous": "^1.0.1",
"passport-facebook": "~2.0.0", "passport-facebook": "~2.0.0",
"passport-github": "~1.0.0", "passport-github": "~1.0.0",
"passport-google-oauth": "~0.2.0", "passport-google-oauth": "~0.2.0",
@ -82,6 +83,7 @@
"random-js": "^1.0.8", "random-js": "^1.0.8",
"raven": "^0.9.0", "raven": "^0.9.0",
"soap": "^0.11.0", "soap": "^0.11.0",
"socket.io": "^1.4.6",
"swig": "~1.4.1" "swig": "~1.4.1"
}, },
"devDependencies": { "devDependencies": {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,55 @@
(function () {
'use strict';
// Create the Socket.io wrapper service
angular
.module('core')
.factory('Socket', Socket);
Socket.$inject = ['$timeout', '$window'];
function Socket($timeout, $window) {
var service = {
connect: connect,
emit: emit,
on: on,
removeListener: removeListener,
socket: null
};
console.log('https://'+window.location.hostname+':'+$window.socketPort);
connect('https://'+window.location.hostname+':'+$window.socketPort);
return service;
// Connect to Socket.io server
function connect(url) {
service.socket = io();
}
// Wrap the Socket.io 'emit' method
function emit(eventName, data) {
if (service.socket) {
service.socket.emit(eventName, data);
}
}
// Wrap the Socket.io 'on' method
function on(eventName, callback) {
if (service.socket) {
service.socket.on(eventName, function (data) {
$timeout(function () {
callback(data);
});
});
}
}
// Wrap the Socket.io 'removeListener' method
function removeListener(eventName) {
if (service.socket) {
service.socket.removeListener(eventName);
}
}
}
}());

View file

@ -79,7 +79,7 @@ angular.module('forms').directive('autoSaveForm', ['$rootScope', '$timeout', fun
} }
}); });
//Autosave Form when model (specificed in $attrs.autoSaveWatch) changes //Autosave Form when model (specified in $attrs.autoSaveWatch) changes
$scope.$watch($attrs.autoSaveWatch, function(newValue, oldValue) { $scope.$watch($attrs.autoSaveWatch, function(newValue, oldValue) {
newValue = angular.copy(newValue); newValue = angular.copy(newValue);

View file

@ -24,8 +24,25 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
forcePlaceholderSize: true forcePlaceholderSize: true
}; };
/*
console.log($scope.sortableOptions); ** Setup Angular-Input-Star Shape Dropdown
*/
//Populate Name to Font-awesomeName Conversion Map
$scope.select2FA = {
'Heart': 'Heart',
'Star': 'Star',
'thumbs-up': 'Thumbs Up',
'thumbs-down':'Thumbs Down',
'Circle': 'Circle',
'Square':'Square',
'Check Circle': 'Checkmark',
'Smile Outlined': 'Smile',
'Hourglass': 'Hourglass',
'bell': 'Bell',
'Paper Plane': 'Paper Plane',
'Comment': 'Chat Bubble',
'Trash': 'Trash Can'
};
//Populate AddField with all available form field types //Populate AddField with all available form field types
$scope.addField = {}; $scope.addField = {};
@ -67,11 +84,11 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
/* /*
** FormFields (ui-sortable) drag-and-drop configuration ** FormFields (ui-sortable) drag-and-drop configuration
*/ */
$scope.dropzone = { $scope.dropzone = {
handle: ' .handle', handle: '.handle',
containment: '.dropzoneContainer', containment: '.dropzoneContainer',
cursor: 'grabbing' cursor: 'grabbing'
}; };
/* /*
** Field CRUD Methods ** Field CRUD Methods
@ -79,7 +96,7 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
// Add a new field // Add a new field
$scope.addNewField = function(modifyForm, fieldType){ $scope.addNewField = function(modifyForm, fieldType){
// incr field_id counter // increment lastAddedID counter
$scope.addField.lastAddedID++; $scope.addField.lastAddedID++;
var fieldTitle; var fieldTitle;
@ -98,12 +115,19 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
disabled: false, disabled: false,
deletePreserved: false deletePreserved: false
}; };
// console.log('\n\n---------\nAdded field CLIENT');
// console.log(newField);
// newField._id = _.uniqueId();
// put newField into fields array if($scope.showAddOptions(newField)){
newField.fieldOptions = [];
newField.fieldOptions.push({
'option_id' : Math.floor(100000*Math.random()), //Generate pseudo-random option id
'option_title' : 'Option 0',
'option_value' : 'Option 0'
});
}
if(modifyForm){ if(modifyForm){
//Add newField to form_fields array
$scope.myform.form_fields.push(newField); $scope.myform.form_fields.push(newField);
} }
return newField; return newField;
@ -168,20 +192,17 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
// add new option to the field // add new option to the field
$scope.addOption = function(field_index){ $scope.addOption = function(field_index){
var currField = $scope.myform.form_fields[field_index]; var currField = $scope.myform.form_fields[field_index];
console.log(field_index); //console.log(field_index);
console.log(currField); //console.log(currField);
if(currField.fieldType === 'checkbox' || currField.fieldType === 'dropdown' || currField.fieldType === 'radio'){ if(currField.fieldType === 'checkbox' || currField.fieldType === 'dropdown' || currField.fieldType === 'radio'){
if(!currField.fieldOptions) $scope.myform.form_fields[field_index].fieldOptions = []; if(!currField.fieldOptions){
$scope.myform.form_fields[field_index].fieldOptions = [];
}
var lastOptionID = 0; var lastOptionID = $scope.myform.form_fields[field_index].fieldOptions.length+1;
if(currField.fieldOptions[currField.fieldOptions.length-1]){
lastOptionID = currField.fieldOptions[currField.fieldOptions.length-1].option_id;
}
// new option's id // new option's id
var option_id = lastOptionID + 1;
var newOption = { var newOption = {
'option_id' : Math.floor(100000*Math.random()), 'option_id' : Math.floor(100000*Math.random()),
@ -219,7 +240,17 @@ angular.module('forms').directive('editFormDirective', ['$rootScope', 'FormField
} }
}; };
} // decides whether field options block will be shown (true for dropdown and radio fields)
$scope.showRatingOptions = function (field){
if(field.fieldType === 'rating'){
return true;
} else {
return false;
}
};
}
}; };
} }

View file

@ -271,6 +271,22 @@
<!-- <span class="required-error" ng-show="field.required && !field.fieldValue">* required</span> --> <!-- <span class="required-error" ng-show="field.required && !field.fieldValue">* required</span> -->
</div> </div>
</div> </div>
<div class="row field">
<div class="field-title col-sm-4">
<h5>Google Analytics Tracking Code</h5>
</div>
<div class="col-sm-8">
<input type="text"
ng-model="myform.analytics.gaCode"
value="{{myform.analytics.gaCode}}"
style="width: 100%;"
ng-minlength="4"
placeholder="UA-XXXXX-Y"
ng-pattern="/\bUA-\d{4,10}-\d{1,4}\b/">
</div>
</div>
<div class="row field"> <div class="row field">
<div class="col-xs-6 field-title">Language</div> <div class="col-xs-6 field-title">Language</div>

View file

@ -220,13 +220,13 @@
<div class="row"><br></div> <div class="row"><br></div>
<div class="row description"> <div class="row description" ng-hide="showRatingOptions(field)">
<div class="col-md-4 col-sm-12">Description:</div> <div class="col-md-4 col-sm-12">Description:</div>
<div class="col-md-8 col-sm-12"><textarea type="text" ng-model="field.description" name="description{{field._id}}"value="{{field.description}}"></textarea> </div> <div class="col-md-8 col-sm-12"><textarea type="text" ng-model="field.description" name="description{{field._id}}"value="{{field.description}}"></textarea> </div>
</div> </div>
<div class="row" ng-show="showAddOptions(field)"><br></div> <div class="row" ng-show="showAddOptions(field)"><br></div>
<div class="row options" ng-show="showAddOptions(field)"> <div class="row options" ng-if="showAddOptions(field)">
<div class="col-md-4 col-xs-12">Options:</div> <div class="col-md-4 col-xs-12">Options:</div>
<div class="col-md-8 col-xs-12"> <div class="col-md-8 col-xs-12">
<div ng-repeat="option in field.fieldOptions track by option.option_id" class="row"> <div ng-repeat="option in field.fieldOptions track by option.option_id" class="row">
@ -244,6 +244,32 @@
</div> </div>
</div> </div>
<div class="row" ng-show="showRatingOptions(field)"><br></div>
<div class="row" ng-if="showRatingOptions(field)">
<div class="col-md-9 col-sm-9">Number of Steps:</div>
<div class="col-md-3 col-sm-3">
<input style="width:100%" type="number"
min="1" max="10"
ng-model="field.ratingOptions.steps"
name="ratingOptions_steps{{field._id}}"
ng-value="{{field.ratingOptions.steps}}"
required>
</div>
<br>
<div class="col-md-5 col-sm-9">Shape:</div>
<div class="col-md-7 col-sm-3">
<select style="width:100%" ng-model="field.ratingOptions.shape"
value="{{field.ratingOptions.steps}}"
name="ratingOptions_shape{{field._id}}" required>
<option ng-repeat="shapeType in field.ratingOptions.validShapes"
value="{{shapeType}}">
{{select2FA[shapeType]}}
</option>
</select>
</div>
</div>
<div class="row"><br></div> <div class="row"><br></div>
<div class="row"> <div class="row">

View file

@ -1,5 +1,47 @@
<div class="submissions-table row container" ng-init="initFormSubmissions()"> <div class="submissions-table row container" ng-init="initFormSubmissions()">
<div class="row">
<div class="col-xs-4">
Total Views: {{myform.analytics.views}}
</div>
<div class="col-xs-4">
Submissions: {{myform.analytics.submissions}}
</div>
<div class="col-xs-4">
Conversion Rate: {{myform.analytics.conversionRate}}%
</div>
</div>
<br>
<div class="row">
<div class="col-xs-12">
<div class="col-xs-2">
<strong>Field Title</strong>
</div>
<div class="col-xs-2">
<strong>Field Views</strong>
</div>
<div class="col-xs-4">
<strong>User dropoff rate at this field</strong>
</div>
</div>
<div class="col-xs-12" ng-repeat="fieldStats in myform.analytics.fields">
<div class="col-xs-2">
{{fieldStats.field.title}}
</div>
<div class="col-xs-2">
{{fieldStats.totalViews}}
</div>
<div class="col-xs-4">
{{fieldStats.dropoffRate}}%
</div>
</div>
</div>
<br>
<div class="row"> <div class="row">
<div class="col-xs-2"> <div class="col-xs-2">
<button class="btn btn-danger" ng-click="deleteSelectedSubmissions()" ng-disabled="!isAtLeastOneChecked();"> <button class="btn btn-danger" ng-click="deleteSelectedSubmissions()" ng-disabled="!isAtLeastOneChecked();">
@ -69,11 +111,8 @@
<th class="scope"> <th class="scope">
{{$index+1}} {{$index+1}}
</th> </th>
<td ng-if="field.fieldType == 'dropdown'" data-ng-repeat="field in row.form_fields">
{{field.fieldValue.field_title}} <td data-ng-repeat="field in row.form_fields">
</td>
<td ng-if="field.fieldType != 'dropdown'" data-ng-repeat="field in row.form_fields">
{{field.fieldValue}} {{field.fieldValue}}
</td> </td>
<td ng-if="myform.plugins.oscarhost.baseUrl"> <td ng-if="myform.plugins.oscarhost.baseUrl">

View file

@ -237,12 +237,6 @@ form .row.field {
form .row.field.dropdown > .field-input input:focus { form .row.field.dropdown > .field-input input:focus {
border: none; border: none;
} }
form .row.field.dropdown > .field-input .ui-select-match {
border: 0 grey solid;
border-width: 0 0 2px 0;
border-radius: 5px;
}
form .dropdown > .field-input .ui-select-choices-row-inner { form .dropdown > .field-input .ui-select-choices-row-inner {
border-radius: 3px; border-radius: 3px;

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
angular.module('forms').directive('fieldIconDirective', function() { angular.module('forms').directive('fieldIconDirective', function() {
return { return {
template: '<i class="{{typeIcon}}"></i>', template: '<i class="{{typeIcon}}"></i>',
restrict: 'E', restrict: 'E',
@ -28,6 +28,6 @@ angular.module('forms').directive('fieldIconDirective', function() {
'number': 'fa fa-slack' 'number': 'fa fa-slack'
}; };
$scope.typeIcon = iconTypeMap[$scope.typeName]; $scope.typeIcon = iconTypeMap[$scope.typeName];
}, }
}; };
}); });

View file

@ -13,9 +13,24 @@ angular.module('forms').directive('fieldDirective', ['$http', '$compile', '$root
var getTemplateUrl = function(fieldType) { var getTemplateUrl = function(fieldType) {
var type = fieldType; var type = fieldType;
var templateUrl = 'modules/forms/base/views/directiveViews/field/';
if (__indexOf.call(supportedFields, type) >= 0) { var supported_fields = [
'textfield',
'textarea',
'date',
'dropdown',
'hidden',
'password',
'radio',
'legal',
'statement',
'rating',
'yes_no',
'number',
'natural'
];
if (__indexOf.call(supported_fields, type) >= 0) {
var templateUrl = 'modules/forms/views/directiveViews/field/';
templateUrl = templateUrl+type+'.html'; templateUrl = templateUrl+type+'.html';
} }
return $templateCache.get(templateUrl); return $templateCache.get(templateUrl);
@ -24,7 +39,7 @@ angular.module('forms').directive('fieldDirective', ['$http', '$compile', '$root
return { return {
template: '<div>{{field.title}}</div>', template: '<div>{{field.title}}</div>',
restrict: 'E', restrict: 'E',
scope: { scope: {
field: '=', field: '=',
required: '&', required: '&',
design: '=', design: '=',
@ -85,7 +100,7 @@ angular.module('forms').directive('fieldDirective', ['$http', '$compile', '$root
} }
var template = getTemplateUrl(fieldType); var template = getTemplateUrl(fieldType);
element.html(template).show(); element.html(template).show();
$compile(element.contents())(scope); var output = $compile(element.contents())(scope);
} }
}; };
}]); }]);

View file

@ -1,12 +1,18 @@
'use strict'; 'use strict';
//TODO: DAVID: Need to refactor this
angular.module('forms').directive('onEnterKey', ['$rootScope', function($rootScope){ angular.module('forms').directive('onEnterKey', ['$rootScope', function($rootScope){
return { return {
restrict: 'A', restrict: 'A',
link: function($scope, $element, $attrs) { link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) { $element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode; var keyCode = event.which || event.keyCode;
if(keyCode === 13 && !event.shiftKey) {
var onEnterKeyDisabled = false;
if($attrs.onEnterKeyDisabled !== null) onEnterKeyDisabled = $attrs.onEnterKeyDisabled;
if(keyCode === 13 && !event.shiftKey && !onEnterKeyDisabled) {
event.preventDefault(); event.preventDefault();
$rootScope.$apply(function() { $rootScope.$apply(function() {
$rootScope.$eval($attrs.onEnterKey); $rootScope.$eval($attrs.onEnterKey);
@ -15,4 +21,56 @@ angular.module('forms').directive('onEnterKey', ['$rootScope', function($rootSco
}); });
} }
}; };
}]).directive('onTabKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if(keyCode === 9 && !event.shiftKey) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onTabKey);
});
}
});
}
};
}]).directive('onEnterOrTabKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if((keyCode === 13 || keyCode === 9) && !event.shiftKey) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onEnterOrTabKey);
});
}
});
}
};
}]).directive('onTabAndShiftKey', ['$rootScope', function($rootScope){
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
$element.bind('keydown keypress', function(event) {
var keyCode = event.which || event.keyCode;
if(keyCode === 9 && event.shiftKey) {
event.preventDefault();
$rootScope.$apply(function() {
$rootScope.$eval($attrs.onTabAndShiftKey);
});
}
});
}
};
}]); }]);

View file

@ -4,7 +4,7 @@ angular.module('forms').directive('onFinishRender', function ($rootScope, $timeo
return { return {
restrict: 'A', restrict: 'A',
link: function (scope, element, attrs) { link: function (scope, element, attrs) {
//Don't do anything if we don't have a ng-repeat on the current element //Don't do anything if we don't have a ng-repeat on the current element
if(!element.attr('ng-repeat') && !element.attr('data-ng-repeat')){ if(!element.attr('ng-repeat') && !element.attr('data-ng-repeat')){
return; return;

View file

@ -1,8 +1,8 @@
'use strict'; 'use strict';
angular.module('forms').directive('submitFormDirective',
['$http', 'TimeCounter', '$filter', '$rootScope', 'Auth', angular.module('forms').directive('submitFormDirective', ['$http', 'TimeCounter', '$filter', '$rootScope', 'Auth', 'SendVisitorData',
function ($http, TimeCounter, $filter, $rootScope, Auth) { function ($http, TimeCounter, $filter, $rootScope, Auth, SendVisitorData) {
return { return {
templateUrl: 'modules/forms/base/views/directiveViews/form/submit-form.client.view.html', restrict: 'E', templateUrl: 'modules/forms/base/views/directiveViews/form/submit-form.client.view.html', restrict: 'E',
scope: { scope: {
@ -49,6 +49,7 @@ angular.module('forms').directive('submitFormDirective',
TimeCounter.restartClock(); TimeCounter.restartClock();
}; };
//Fire event when window is scrolled
$window.onscroll = function(){ $window.onscroll = function(){
$scope.scrollPos = document.body.scrollTop || document.documentElement.scrollTop || 0; $scope.scrollPos = document.body.scrollTop || document.documentElement.scrollTop || 0;
var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect(); var elemBox = document.getElementsByClassName('activeField')[0].getBoundingClientRect();
@ -88,13 +89,22 @@ angular.module('forms').directive('submitFormDirective',
} }
}; };
$rootScope.setDropdownOption = function(){
console.log('setDropdownOption index: ');
};
/* /*
** Field Controls ** Field Controls
*/ */
var getActiveField = function(){
if($scope.selected === null){
console.error('current active field is null');
throw new Error('current active field is null');
}
if($scope.selected._id === 'submit_field') {
return $scope.myform.form_fields.length - 1;
} else {
return $scope.selected.index;
}
};
$scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) { $scope.setActiveField = $rootScope.setActiveField = function(field_id, field_index, animateScroll) {
if($scope.selected === null || $scope.selected._id === field_id){ if($scope.selected === null || $scope.selected._id === field_id){
//console.log('not scrolling'); //console.log('not scrolling');
@ -121,12 +131,15 @@ angular.module('forms').directive('submitFormDirective',
$document.scrollToElement(angular.element('.activeField'), -10, 200).then(function() { $document.scrollToElement(angular.element('.activeField'), -10, 200).then(function() {
$scope.noscroll = false; $scope.noscroll = false;
setTimeout(function() { setTimeout(function() {
if (document.querySelectorAll('.activeField .focusOn')[0]) { if (document.querySelectorAll('.activeField .focusOn').length) {
//console.log(document.querySelectorAll('.activeField .focusOn')[0]); //Handle default case
document.querySelectorAll('.activeField .focusOn')[0].focus(); document.querySelectorAll('.activeField .focusOn')[0].focus();
} else { } else if(document.querySelectorAll('.activeField input').length) {
//console.log(document.querySelectorAll('.activeField input')[0]); //Handle case for rating input
document.querySelectorAll('.activeField input')[0].focus(); document.querySelectorAll('.activeField input')[0].focus();
} else {
//Handle case for dropdown input
document.querySelectorAll('.activeField .selectize-input')[0].focus();
} }
}); });
}); });
@ -134,13 +147,15 @@ angular.module('forms').directive('submitFormDirective',
}else { }else {
setTimeout(function() { setTimeout(function() {
if (document.querySelectorAll('.activeField .focusOn')[0]) { if (document.querySelectorAll('.activeField .focusOn')[0]) {
//console.log(document.querySelectorAll('.activeField .focusOn')[0]); //FIXME: DAVID: Figure out how to set focus without scroll movement in HTML Dom
document.querySelectorAll('.activeField .focusOn')[0].focus(); document.querySelectorAll('.activeField .focusOn')[0].focus();
} else { } else {
document.querySelectorAll('.activeField input')[0].focus(); document.querySelectorAll('.activeField input')[0].focus();
} }
}); });
} }
SendVisitorData.send($scope.myform, getActiveField(), TimeCounter.getTimeElapsed());
}; };
$rootScope.nextField = $scope.nextField = function(){ $rootScope.nextField = $scope.nextField = function(){
@ -178,30 +193,39 @@ angular.module('forms').directive('submitFormDirective',
} }
}; };
$scope.goToInvalid = function() { $rootScope.goToInvalid = $scope.goToInvalid = function() {
document.querySelectorAll('.ng-invalid.focusOn')[0].focus(); document.querySelectorAll('.ng-invalid.focusOn')[0].focus();
}; };
$scope.submitForm = function() { $rootScope.submitForm = $scope.submitForm = function() {
var _timeElapsed = TimeCounter.stopClock(); var _timeElapsed = TimeCounter.stopClock();
$scope.loading = true; $scope.loading = true;
var form = _.cloneDeep($scope.myform); var form = _.cloneDeep($scope.myform);
form.timeElapsed = _timeElapsed; form.timeElapsed = _timeElapsed;
form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100; form.percentageComplete = $filter('formValidity')($scope.myform) / $scope.myform.visible_form_fields.length * 100;
delete form.visible_form_fields; delete form.visible_form_fields;
for(var i=0; i < $scope.myform.form_fields.length; i++){
if($scope.myform.form_fields[i].fieldType === 'dropdown' && !$scope.myform.form_fields[i].deletePreserved){
$scope.myform.form_fields[i].fieldValue = $scope.myform.form_fields[i].fieldValue.option_value;
}
}
setTimeout(function () { setTimeout(function () {
$scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form) $scope.submitPromise = $http.post('/forms/' + $scope.myform._id, form)
.success(function (data, status, headers) { .success(function (data, status, headers) {
//console.log('form submitted successfully'); console.log($scope.myform.form_fields[0]);
$scope.myform.submitted = true; $scope.myform.submitted = true;
$scope.loading = false; $scope.loading = false;
SendVisitorData.send($scope.myform, getActiveField(), _timeElapsed);
}) })
.error(function (error) { .error(function (error) {
$scope.loading = false; $scope.loading = false;
//console.log(error); console.error(error);
$scope.error = error.message; $scope.error = error.message;
}); });
}, 500); }, 500);

View file

@ -8,7 +8,7 @@ angular.module('forms').factory('Forms', ['$resource', 'FORM_URL',
}, { }, {
'query' : { 'query' : {
method: 'GET', method: 'GET',
isArray: true, isArray: true
//DAVID: TODO: Do we really need to get visible_form_fields for a Query? //DAVID: TODO: Do we really need to get visible_form_fields for a Query?
// transformResponse: function(data, header) { // transformResponse: function(data, header) {
// var forms = angular.fromJson(data); // var forms = angular.fromJson(data);
@ -24,8 +24,8 @@ angular.module('forms').factory('Forms', ['$resource', 'FORM_URL',
method: 'GET', method: 'GET',
transformResponse: function(data, header) { transformResponse: function(data, header) {
var form = angular.fromJson(data); var form = angular.fromJson(data);
//console.log(form);
form.visible_form_fields = _.filter(form.form_fields, function(field){ form.visible_form_fields = _.filter(form.form_fields, function(field){
return (field.deletePreserved === false); return (field.deletePreserved === false);
}); });
return form; return form;

View file

@ -2,22 +2,29 @@
angular.module('forms').service('TimeCounter', [ angular.module('forms').service('TimeCounter', [
function(){ function(){
var _startTime, _endTime, that=this; var _startTime, _endTime = null, that=this;
this.timeSpent = 0; this.timeSpent = 0;
this.restartClock = function(){ this.restartClock = function(){
_startTime = Date.now(); _startTime = Date.now();
_endTime = _startTime; _endTime = null;
// console.log('Clock Started'); // console.log('Clock Started');
}; };
this.getTimeElapsed = function(){
if(_startTime) {
return Math.abs(Date.now().valueOf() - _startTime.valueOf()) / 1000;
}
};
this.stopClock = function(){ this.stopClock = function(){
if(_startTime){ if(_startTime && _endTime === null){
_endTime = Date.now(); _endTime = Date.now();
that.timeSpent = Math.abs(_endTime.valueOf() - _startTime.valueOf())/1000; this.timeSpent = Math.abs(_endTime.valueOf() - _startTime.valueOf())/1000;
// console.log('Clock Ended'); this._startTime = this._endTime = null;
return that.timeSpent;
return this.timeSpent;
}else{ }else{
return new Error('Clock has not been started'); return new Error('Clock has not been started');
} }
@ -28,4 +35,4 @@ angular.module('forms').service('TimeCounter', [
}; };
} }
]); ]);

View file

@ -9,11 +9,13 @@
{{field.title}} {{field.title}}
<span class="required-error" ng-show="!field.required && !field.fieldValue">{{ 'OPTIONAL' | translate }}</span> <span class="required-error" ng-show="!field.required && !field.fieldValue">{{ 'OPTIONAL' | translate }}</span>
</h3> </h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="col-xs-12 field-input"> <div class="col-xs-12 field-input">
<div class="control-group input-append"> <div class="control-group input-append">
<input ng-focus="setActiveField(field._id, index, true)" <input class="focusOn"
class="focusOn"
ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}" ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-class="{ 'no-border': !!field.fieldValue }" ng-class="{ 'no-border': !!field.fieldValue }"
ui-date="dateOptions" ui-date="dateOptions"
@ -22,7 +24,9 @@
ng-required="field.required" ng-required="field.required"
ng-disabled="field.disabled" ng-disabled="field.disabled"
placeholder="MM/DD/YYYY" placeholder="MM/DD/YYYY"
on-enter-key="nextField()" ng-focus="setActiveField(field._id, index, true)"
on-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-change="$root.nextField()"> ng-change="$root.nextField()">
</div> </div>
</div> </div>

View file

@ -1,5 +1,4 @@
<div class="field row dropdown" <div class="field row dropdown"
ng-click="setActiveField(field._id, index, true)"
ng-if="field.fieldOptions.length > 0"> ng-if="field.fieldOptions.length > 0">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3> <h3>
@ -10,15 +9,22 @@
{{field.title}} {{field.title}}
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span> <span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3> </h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="col-xs-12 field-input"> <div class="col-xs-12 field-input">
<ui-select ng-model="field.fieldValue" <ui-select ng-model="field.fieldValue"
theme="selectize" theme="selectize"
search-enabled="true"
search-by="option_value"
set-search-to-answer="true"
ng-required="field.required" ng-required="field.required"
ng-disabled="field.disabled" ng-disabled="field.disabled"
on-tab-and-shift-key="prevField()"
on-tab-key="nextField()"
ng-change="$root.nextField()"> ng-change="$root.nextField()">
<ui-select-match placeholder="Type or select an option"> <ui-select-match placeholder="Type or select an option">
{{$select.selected.option_value}}
</ui-select-match> </ui-select-match>
<ui-select-choices repeat="option in field.fieldOptions | filter: $select.search" <ui-select-choices repeat="option in field.fieldOptions | filter: $select.search"
ng-class="{'active': option.option_value === field.fieldValue }"> ng-class="{'active': option.option_value === field.fieldValue }">

View file

@ -1,5 +1,5 @@
<div class="field row radio legal" <div class="field row radio legal"
on-enter-key="nextField()" on-enter-or-tab-key="nextField()"
key-to-truthy key-char-truthy="y" key-char-falsey="n" field="field"> key-to-truthy key-char-truthy="y" key-char-falsey="n" field="field">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3> <h3>
@ -11,10 +11,12 @@
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span> <span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3> </h3>
<br> <br>
<p style="color:#ddd;">{{field.description}}</p> <p class="col-xs-12">{{field.description}}</p>
</div> </div>
<div class="col-xs-12 field-input container"> <div class="col-xs-12 field-input container">
<div class="row-fluid"> <div class="row-fluid"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()">
<label class="btn col-md-5 col-xs-12" <label class="btn col-md-5 col-xs-12"
ng-class="{activeBtn: field.fieldValue == 'true'}"> ng-class="{activeBtn: field.fieldValue == 'true'}">
<input class="focusOn" <input class="focusOn"

View file

@ -1,5 +1,5 @@
<div class="field row radio" <div class="field row radio"
on-enter-key="nextField()" on-enter-or-tab-key="nextField()"
key-to-option field="field" key-to-option field="field"
ng-if="field.fieldOptions.length > 0"> ng-if="field.fieldOptions.length > 0">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
@ -11,6 +11,9 @@
{{field.title}} {{field.title}}
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span> <span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3> </h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="col-xs-12 field-input"> <div class="col-xs-12 field-input">

View file

@ -1,5 +1,5 @@
<div class="textfield field row" <div class="textfield field row"
on-enter-key="nextField()"> on-enter-or-tab-key="nextField()">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3> <h3>
<small class="field-number"> <small class="field-number">
@ -8,19 +8,27 @@
</small> </small>
{{field.title}} {{field.title}}
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span> <span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3> </h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="col-xs-12 field-input"> <div class="col-xs-12 field-input">
<input-stars max="5"
<input-stars max="{{field.ratingOptions.steps}}"
ng-init="field.fieldValue = 1" ng-init="field.fieldValue = 1"
on-star-click="$root.nextField()" on-star-click="$root.nextField()"
icon-full="fa-star" icon-full="{{field.ratingOptions.shape}}"
icon-base="fa fa-3x" icon-base="fa fa-3x"
icon-empty="fa-star-o" icon-empty="{{field.ratingOptions.shape}}"
ng-model="field.fieldValue" ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }" ng-model-options="{ debounce: 250 }"
ng-required="field.required" ng-required="field.required"
ng-disabled="field.disabled" ng-disabled="field.disabled"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-focus="setActiveField(field._id, index, true)"
class="angular-input-stars focusOn"> class="angular-input-stars focusOn">
</input-stars> </input-stars>
</div> </div>

View file

@ -1,9 +1,13 @@
<div class="statement field row" <div class="statement field row"
on-enter-key="$root.nextField()" on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-focus="setActiveField(field._id, index, true)"> ng-focus="setActiveField(field._id, index, true)">
<div class="row field-title field-title"> <div class="row field-title field-title">
<div class="col-xs-1"><i class="fa fa-quote-left fa-1"></i></div> <div class="col-xs-1"><i class="fa fa-quote-left fa-1"></i></div>
<h2 class="text-left col-xs-9">{{field.title}}</h2> <h2 class="text-left col-xs-9">{{field.title}}</h2>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="row field-title field-input"> <div class="row field-title field-input">
<p class="col-xs-12" ng-if="field.description.length">{{field.description}} </p> <p class="col-xs-12" ng-if="field.description.length">{{field.description}} </p>

View file

@ -1,5 +1,4 @@
<div class="field row" ng-click="setActiveField(field._id, index, true)" <div class="field row" ng-click="setActiveField(field._id, index, true)">
ng-focus="setActiveField(field._id, index, true)">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3> <h3>
<small class="field-number"> <small class="field-number">
@ -10,8 +9,12 @@
<span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span> <span class="required-error" ng-show="!field.required">{{ 'OPTIONAL' | translate }}</span>
</h3> </h3>
<small>{{ 'NEWLINE' | translate }}</small> <small>{{ 'NEWLINE' | translate }}</small>
<p>
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="col-xs-12 field-input"> <div class="col-xs-12 field-input">
<small style="font-size:0.6em;">Press SHIFT+ENTER to add a newline</small>
<textarea class="textarea focusOn" type="text" <textarea class="textarea focusOn" type="text"
ng-model="field.fieldValue" ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }" ng-model-options="{ debounce: 250 }"
@ -19,14 +22,16 @@
value="{{field.fieldValue}}" value="{{field.fieldValue}}"
ng-required="field.required" ng-required="field.required"
ng-disabled="field.disabled" ng-disabled="field.disabled"
ng-focus="setActiveField(field._id, index, true)" ng-focus="setActiveField(field._id, index, true)"
on-enter-key="nextField()"> on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
style="border: none; border-left: lightgrey dashed 2px;">
</textarea> </textarea>
</div> </div>
</div> </div>
<div> <div>
<div class="btn btn-lg btn-default col-xs-12 col-sm-4" <div class="btn btn-lg btn-default col-xs-12 col-sm-4 hidden-xs"
style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)"> style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)">
<button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid" <button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid"
ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}" ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"

View file

@ -1,7 +1,7 @@
<div class="textfield field row" <div class="textfield field row"
ng-click="setActiveField(field._id, index, true)"> ng-click="setActiveField(field._id, index, true)">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title row-fluid" ng-style="{'color': design.colors.questionColor}">
<h3> <h3 class="col-xs-12">
<small class="field-number"> <small class="field-number">
{{index+1}} {{index+1}}
<i class="fa fa-angle-double-right" aria-hidden="true"></i> <i class="fa fa-angle-double-right" aria-hidden="true"></i>
@ -13,10 +13,13 @@
({{ 'OPTIONAL' | translate }}) ({{ 'OPTIONAL' | translate }})
</span> </span>
</h3> </h3>
<p class="col-xs-12">
<small>{{field.description}}</small>
</p>
</div> </div>
<div class="col-xs-12 field-input"> <div class="col-xs-12 field-input">
<input ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}" <input ng-style="{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}"
ng-focus="setActiveField(field._id, index, true)"
name="{{field.fieldType}}{{index}}" name="{{field.fieldType}}{{index}}"
type="{{field.input_type}}" type="{{field.input_type}}"
ng-pattern="field.validateRegex" ng-pattern="field.validateRegex"
@ -26,10 +29,15 @@
ng-model="field.fieldValue" ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }" ng-model-options="{ debounce: 250 }"
value="field.fieldValue" value="field.fieldValue"
ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }"
value="field.fieldValue"
ng-focus="setActiveField(field._id, index, true)"
on-enter-or-tab-key="nextField()"
on-tab-and-shift-key="prevField()"
ng-required="field.required" ng-required="field.required"
ng-disabled="field.disabled" ng-disabled="field.disabled"
aria-describedby="inputError2Status" aria-describedby="inputError2Status">
on-enter-key="nextField()">
</div> </div>
<div class="col-xs-12"> <div class="col-xs-12">
<div ng-show="forms.myForm.{{field.fieldType}}{{index}}.$invalid && !!forms.myForm.{{field.fieldType}}{{index}}.$viewValue " class="alert alert-danger" role="alert"> <div ng-show="forms.myForm.{{field.fieldType}}{{index}}.$invalid && !!forms.myForm.{{field.fieldType}}{{index}}.$viewValue " class="alert alert-danger" role="alert">
@ -42,7 +50,7 @@
</div> </div>
</div> </div>
<div> <div>
<div class="btn btn-lg btn-default col-xs-12 col-sm-4" <div class="btn btn-lg btn-default col-xs-12 col-sm-4 hidden-xs"
style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)"> style="padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)">
<button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid" <button ng-disabled="!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid"
ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}" ng-style="{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}"

View file

@ -1,6 +1,6 @@
<div class="field row radio" <div class="field row radio"
ng-click="setActiveField(field._id, index, true)" ng-click="setActiveField(field._id, index, true)"
on-enter-key="nextField()" on-tab-and-shift-key="prevField()"
key-to-truthy key-char-truthy="y" key-char-falsey="n" field="field"> key-to-truthy key-char-truthy="y" key-char-falsey="n" field="field">
<div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}"> <div class="col-xs-12 field-title" ng-style="{'color': design.colors.questionColor}">
<h3 class="row"> <h3 class="row">
@ -25,10 +25,10 @@
<input type="radio" value="true" <input type="radio" value="true"
class="focusOn" class="focusOn"
style="opacity: 0; margin-left: 0px;" style="opacity: 0; margin-left: 0px;"
ng-focus="setActiveField(field._id, index, true)"
ng-model="field.fieldValue" ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }" ng-focus="setActiveField(field._id, index, true)"
ng-required="field.required" ng-model-options="{ debounce: 250 }"
ng-required="field.required"
ng-change="$root.nextField()" ng-change="$root.nextField()"
ng-disabled="field.disabled" /> ng-disabled="field.disabled" />
<div class="letter"> <div class="letter">
@ -45,10 +45,9 @@
<input type="radio" value="false" <input type="radio" value="false"
style="opacity:0; margin-left:0px;" style="opacity:0; margin-left:0px;"
ng-focus="setActiveField(field._id, index, true)"
ng-model="field.fieldValue" ng-model="field.fieldValue"
ng-model-options="{ debounce: 250 }" ng-model-options="{ debounce: 250 }"
ng-required="field.required" ng-required="field.required"
ng-change="$root.nextField()" ng-change="$root.nextField()"
ng-disabled="field.disabled"/> ng-disabled="field.disabled"/>

View file

@ -53,6 +53,7 @@ ng-style="{'color':button.color}">
data-id="{{field._id}}" data-id="{{field._id}}"
ng-class="{activeField: selected._id == field._id }" ng-class="{activeField: selected._id == field._id }"
class="row field-directive"> class="row field-directive">
<field-directive field="field" design="myform.design" index="$index" forms="forms"> <field-directive field="field" design="myform.design" index="$index" forms="forms">
</field-directive> </field-directive>
</div> </div>
@ -76,8 +77,10 @@ ng-style="{'color':button.color}">
<button ng-if="!forms.myForm.$invalid" <button ng-if="!forms.myForm.$invalid"
class="Button btn col-sm-2 col-xs-8 focusOn" class="Button btn col-sm-2 col-xs-8 focusOn"
v-busy="loading" v-busy-label="Please wait" v-pressable v-busy="loading" v-busy-label="Please wait" v-pressable
ng-disabled="loading" ng-disabled="loading || forms.myForm.$invalid"
ng-click="submitForm()" ng-click="submitForm()"
on-enter-key="submitForm()"
on-enter-key-disabled="loading || forms.myForm.$invalid"
ng-style="{'background-color':myform.design.colors.buttonColor, 'color':myform.design.colors.buttonTextColor}" ng-style="{'background-color':myform.design.colors.buttonColor, 'color':myform.design.colors.buttonTextColor}"
style="font-size: 1.6em; margin-left: 1em; margin-top: 1em;"> style="font-size: 1.6em; margin-left: 1em; margin-top: 1em;">
@ -85,8 +88,10 @@ ng-style="{'color':button.color}">
</button> </button>
<button ng-if="forms.myForm.$invalid" <button ng-if="forms.myForm.$invalid"
class="Button btn col-sm-2 col-xs-8" class="Button btn col-sm-2 col-xs-8 focusOn"
ng-click="goToInvalid()" ng-click="goToInvalid()"
on-enter-key="goToInvalid()"
on-enter-key-disabled="!forms.myForm.$invalid"
style="font-size: 1.6em; margin-left: 1em; margin-top: 1em; background-color:#990000; color:white"> style="font-size: 1.6em; margin-left: 1em; margin-top: 1em; background-color:#990000; color:white">
{{ 'REVIEW' | translate }} {{ 'REVIEW' | translate }}
</button> </button>

View file

@ -1,4 +1,17 @@
<section class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }"> <section class="public-form" ng-style="{ 'background-color': myform.design.colors.backgroundColor }">
<submit-form-directive myform="myform"></submit-form-directive> <submit-form-directive myform="myform"></submit-form-directive>
</section> </section>
<!-- User's Google Analytics -->
<script ng-if="myform.analytics.gaCode">
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{myform.analytics.gaCode}}', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

View file

@ -0,0 +1,44 @@
(function () {
'use strict';
// Create the SendVisitorData service
angular
.module('forms')
.factory('SendVisitorData', SendVisitorData);
SendVisitorData.$inject = ['Socket', '$state'];
function SendVisitorData(Socket, $state) {
// Create a controller method for sending visitor data
function send(form, lastActiveIndex, timeElapsed) {
console.log(lastActiveIndex);
// Create a new message object
var visitorData = {
referrer: document.referrer,
isSubmitted: form.submitted,
formId: form._id,
lastActiveField: form.form_fields[lastActiveIndex]._id,
timeElapsed: timeElapsed
};
Socket.emit('form-visitor-data', visitorData);
}
function init(){
// Make sure the Socket is connected
if (!Socket.socket) {
Socket.connect();
}
}
var service = {
send: send
};
init();
return service;
}
}());

View file

@ -68,7 +68,7 @@ angular.module('NodeForm.templates', []).run(['$templateCache', function($templa
"<div class=\"field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><textarea class=textarea type=text ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-class=\"{ 'no-border': !!field.fieldValue }\" value={{field.fieldValue}} class=focusOn ng-required=field.required ng-disabled=field.disabled ng-focus=\"setActiveField(field._id, index, true)\">\n" + "<div class=\"field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><textarea class=textarea type=text ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-class=\"{ 'no-border': !!field.fieldValue }\" value={{field.fieldValue}} class=focusOn ng-required=field.required ng-disabled=field.disabled ng-focus=\"setActiveField(field._id, index, true)\">\n" +
" </textarea></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=!field.fieldValue ng-click=$root.nextField() ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>"); " </textarea></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=!field.fieldValue ng-click=$root.nextField() ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>");
$templateCache.put("../public/modules/forms/views/directiveViews/field/textfield.html", $templateCache.put("../public/modules/forms/views/directiveViews/field/textfield.html",
"<div class=\"textfield field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><input ng-style=\"{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}\" ng-focus=\"setActiveField(field._id, index, true)\" name={{field.fieldType}}{{index}} type={{field.input_type}} placeholder={{field.placeholder}} ng-class=\"{ 'no-border': !!field.fieldValue }\" class=\"focusOn text-field-input\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" value=field.fieldValue ng-required=field.required ng-disabled=field.disabled aria-describedby=inputError2Status on-enter-key=nextField()></div><div class=col-xs-12><div ng-show=forms.myForm.{{field.fieldType}}{{index}}.$invalid class=\"alert alert-danger\" role=alert><span class=\"glyphicon glyphicon-exclamation-sign\" aria-hidden=true></span> <span class=sr-only>Error:</span> Enter a valid email address</div></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=\"!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid\" ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" ng-click=$root.nextField() class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>"); "<div class=\"textfield field row\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\" ng-style=\"{'color': design.colors.questionColor}\"><h3><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3></div><div class=\"col-xs-12 field-input\"><input ng-style=\"{'color': design.colors.answerColor, 'border-color': design.colors.answerColor}\" ng-focus=\"setActiveField(field._id, index, true)\" name={{field.fieldType}}{{index}} type={{field.input_type}} placeholder={{field.placeholder}} ng-class=\"{ 'no-border': !!field.fieldValue }\" class=\"focusOn text-field-input\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" value=field.fieldValue ng-required=field.required ng-disabled=field.disabled aria-describedby=inputError2Status on-enter-or-tab-key=nextField()></div><div class=col-xs-12><div ng-show=forms.myForm.{{field.fieldType}}{{index}}.$invalid class=\"alert alert-danger\" role=alert><span class=\"glyphicon glyphicon-exclamation-sign\" aria-hidden=true></span> <span class=sr-only>Error:</span> Enter a valid email address</div></div></div><div class=\"col-xs-12 row\"><div class=\"btn btn-lg btn-default row-fluid\" style=\"padding: 4px; margin-top:8px; background: rgba(255,255,255,0.5)\"><button ng-disabled=\"!field.fieldValue || forms.myForm.{{field.fieldType}}{{$index}}.$invalid\" ng-style=\"{'background-color':design.colors.buttonColor, 'color':design.colors.buttonTextColor}\" ng-click=$root.nextField() class=\"btn col-sm-5 col-xs-5\">OK <i class=\"fa fa-check\"></i></button><div class=\"col-sm-3 col-xs-6\" style=margin-top:0.2em><small style=\"color:#ddd; font-size:70%\">press ENTER</small></div></div></div>");
$templateCache.put("../public/modules/forms/views/directiveViews/field/yes_no.html", $templateCache.put("../public/modules/forms/views/directiveViews/field/yes_no.html",
"<div class=\"field row radio\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\"><h3 class=row><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3><p class=row>{{field.description}}</p></div><div class=\"col-xs-12 field-input\"><div class=row><label class=\"btn btn-default col-md-2 col-sm-3 col-xs-4\" style=\"background: rgba(0,0,0,0.1); text-align:left\"><input type=radio value=true class=focusOn style=\"opacity: 0; margin-left: 0px\" ng-focus=\"setActiveField(field._id, index, true)\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-required=field.required ng-click=$root.nextField() ng-disabled=\"field.disabled\"><div class=letter>Y</div><span>Yes</span> <i ng-show=\"field.fieldValue === 'true'\" class=\"fa fa-check\" aria-hidden=true></i></label></div><div class=row style=\"margin-top: 10px\"><label class=\"btn btn-default col-md-2 col-sm-3 col-xs-4\" style=\"background: rgba(0,0,0,0.1); text-align:left\"><input type=radio value=false style=\"opacity:0; margin-left:0px\" ng-focus=\"setActiveField(field._id, index, true)\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-required=field.required ng-click=$root.nextField() ng-disabled=\"field.disabled\"><div class=letter>N</div><span>No</span> <i ng-show=\"field.fieldValue === 'false'\" class=\"fa fa-check\" aria-hidden=true></i></label></div></div></div><br>"); "<div class=\"field row radio\" ng-click=\"setActiveField(field._id, index, true)\"><div class=\"col-xs-12 field-title\" ng-style=\"{'color': design.colors.questionColor}\"><h3 class=row><span class=\"fa fa-angle-double-right\"></span> {{field.title}} <span class=required-error ng-show=\"field.required && !field.fieldValue\">*(required)</span></h3><p class=row>{{field.description}}</p></div><div class=\"col-xs-12 field-input\"><div class=row><label class=\"btn btn-default col-md-2 col-sm-3 col-xs-4\" style=\"background: rgba(0,0,0,0.1); text-align:left\"><input type=radio value=true class=focusOn style=\"opacity: 0; margin-left: 0px\" ng-focus=\"setActiveField(field._id, index, true)\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-required=field.required ng-click=$root.nextField() ng-disabled=\"field.disabled\"><div class=letter>Y</div><span>Yes</span> <i ng-show=\"field.fieldValue === 'true'\" class=\"fa fa-check\" aria-hidden=true></i></label></div><div class=row style=\"margin-top: 10px\"><label class=\"btn btn-default col-md-2 col-sm-3 col-xs-4\" style=\"background: rgba(0,0,0,0.1); text-align:left\"><input type=radio value=false style=\"opacity:0; margin-left:0px\" ng-focus=\"setActiveField(field._id, index, true)\" ng-model=field.fieldValue ng-model-options=\"{ debounce: 250 }\" ng-required=field.required ng-click=$root.nextField() ng-disabled=\"field.disabled\"><div class=letter>N</div><span>No</span> <i ng-show=\"field.fieldValue === 'false'\" class=\"fa fa-check\" aria-hidden=true></i></label></div></div></div><br>");
$templateCache.put("../public/modules/forms/views/directiveViews/form/configure-form.client.view.html", $templateCache.put("../public/modules/forms/views/directiveViews/form/configure-form.client.view.html",

View file

@ -172,50 +172,51 @@ var questions = [
} }
]; ];
console.log(chalk.green('\n\nHi, welcome to TellForm Setup')); if(!fs.existsSync('./\.env')) {
console.log(chalk.green('\n\nHi, welcome to TellForm Setup'));
console.log(chalk.green('You should only run this the first time you setup TellForm\n--------------------------------------------------\n\n')); console.log(chalk.green('You should only run this the first time you setup TellForm\n--------------------------------------------------\n\n'));
inquirer.prompt([questions[0]]).then(function (confirmAns) { inquirer.prompt([questions[0]]).then(function (confirmAns) {
if(confirmAns['shouldContinue']) { if (confirmAns['shouldContinue']) {
inquirer.prompt(questions.slice(1)).then(function (answers) { inquirer.prompt(questions.slice(1)).then(function (answers) {
answers['NODE_ENV'] = 'production'; answers['NODE_ENV'] = 'production';
answers['SIGNUP_DISABLED'] = false ? answers['SIGNUP_DISABLED'] === false : true; answers['SIGNUP_DISABLED'] = false ? answers['SIGNUP_DISABLED'] === false : true;
var email = answers['email'];
var pass = answers['password'];
delete answers['email'];
delete answers['password'];
var email = answers['email']; envfile.stringify(answers, function (err, str) {
var pass = answers['password']; fs.outputFile('./\.env', str, function (err) {
delete answers['email']; if (err) return console.error(chalk.red(err));
delete answers['password']; console.log(chalk.green('Successfully created .env file'));
});
user = new User({
firstName: 'Admin',
lastName: 'Account',
email: email,
username: email,
password: pass,
provider: 'local',
roles: ['admin', 'user']
});
envfile.stringify(answers, function (err, str) { user.save(function (err) {
fs.outputFile('..//.env', str, function(err){ if (err) return console.error(chalk.red(err));
if (err) return console.error(chalk.red(err)); console.log(chalk.green('Successfully created user'));
console.log(chalk.green('Successfully created .env file')); delete email;
}); delete pass;
user = new User({
firstName: 'Admin',
lastName: 'Account',
email: email,
username: email,
password: pass,
provider: 'local',
roles: ['admin', 'user']
});
user.save(function (err) { console.log(chalk.green('Have fun using TellForm!'));
if (err) return console.error(chalk.red(err)); process.exit(1);
console.log(chalk.green('Successfully created user')); });
delete email;
delete pass;
console.log(chalk.green('Have fun using TellForm!'));
process.exit(1);
}); });
}); });
}); } else {
} else { console.log(chalk.green('Have fun using TellForm!'));
console.log(chalk.green('Have fun using TellForm!')); }
} });
}); }

View file

@ -53,7 +53,7 @@ if (process.env.NODE_ENV === 'secure') {
console.log('--'); console.log('--');
process.on('uncaughtException', function (err) { process.on('uncaughtException', function (err) {
console.error((new Date).toUTCString() + ' uncaughtException:', err.message); console.error((new Date()).toUTCString() + ' uncaughtException:', err.message);
console.error(err.stack); console.error(err.stack);
process.exit(1); process.exit(1);
}); });