Skip to content

Instantly share code, notes, and snippets.

@bowheart
Last active October 20, 2016 15:07
Show Gist options
  • Save bowheart/590fa7c295e2733277a1 to your computer and use it in GitHub Desktop.
Save bowheart/590fa7c295e2733277a1 to your computer and use it in GitHub Desktop.
When the user is filling out input fields, it's nice to have some visual feedback. This library performs non-strict input validation. It highlights fields red or green and animates a little 'x' or checkmark in from the right. This helps the user know if they made a mistake and encourages them to keep going.
/**
* Another library by Joshua Claunch
* https://github.com/bowheart
* https://gist.github.com/bowheart
*
* A jQuery-dependent form feedback library.
*/
$(function() {
var fontIsLoaded = function(fontName, content, callback) {
callback || (callback = content) && (content = '');
var testSpan = $('<span>' + (content || 'mmmmwwww') + '</span>');
testSpan.css({
fontFamily: 'arial',
fontSize: '100px',
position: 'absolute',
top: '-999%',
left: '-999%',
visibility: 'hidden'
});
$('body').append(testSpan);
var testSize = testSpan.width();
testSpan.css('fontFamily', fontName + ', arial');
var counter = 0;
var interval = window.setInterval(function() {
var resultSize = testSpan.width();
var isLoaded = testSize !== resultSize;
if (++counter >= 20 || isLoaded) {
testSpan.remove();
window.clearInterval(interval);
callback(isLoaded);
}
}, 50);
};
// Make sure font-awesome gets loaded
fontIsLoaded('FontAwesome', 'aa<i class="fa-times"></i>', function(isLoaded) {
if (isLoaded) return;
$('head').append('<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">');
});
var defaultConfig = {
selector: 'form input:not([type="submit"]):not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), form select',
colors: {
invalid: '#d44',
invalidLight: '#fff1f1',
valid: '#4d4',
validLight: '#f1fff1'
},
animationDistance: 20,
borderWidth: 1,
markerSize: 16,
offset: 0,
overrideSelects: true,
validation: {
creditcard: function(val) { return val.indexOf('_') === -1; },
cvv: function(val) { return val.length === 3 || val.length === 4; },
email: function(val) { return /^[A-Z0-9\.\-_\+]+@([A-Z0-9\-]+\.)+[A-Z]{2,4}$/.test(val.toUpperCase()); },
phone: function(val) { return val.indexOf('_') === -1 && val.length >=10; },
zip: (function() {
var length = Number($('input[name="zip"], input[name="billingZip"]').first().attr('maxlength'));
return function(val) { return val.length === length || val.length >= 6; };
})()
}
};
// Allow user to define custom config (window.confirmationConfig). Put that into the default config:
var config = $.extend(true, {}, defaultConfig, window.confirmationConfig);
// Point the global config object to the combined config object now -- Allow user to change values later
//window.confirmationConfig = config; // not implementing for now.
var css = '.valid-marker { color: ' + config.colors.valid + '; font-size: ' + config.markerSize + 'px; opacity: 0; position: absolute; top: 0; right: 0; }'
+ '.valid-marker i { display: inline-block; vertical-align: middle; }'
+ '.valid-marker:after { content: ""; display: inline-block; height: 100%; vertical-align: middle; }'
+ '.invalid-input {'
+ 'border-color: ' + config.colors.invalid + ' !important;'
+ 'border-style: solid !important;'
+ 'box-shadow: inset 0 0 0 200px ' + config.colors.invalidLight + ', 0 0 0 ' + config.borderWidth + 'px ' + config.colors.invalid + '; }'
+ '.invalid-input + .valid-marker { color: ' + config.colors.invalid + '; }'
+ '.valid-input {'
+ 'border-color: ' + config.colors.valid + ' !important;'
+ 'border-style: solid !important;'
+ 'box-shadow: inset 0 0 0 200px ' + config.colors.validLight + ', 0 0 0 ' + config.borderWidth + 'px ' + config.colors.valid + '; }';
var blob = new Blob([css], {type: 'text/css'});
$('head').append('<link rel="stylesheet" href="' + window.URL.createObjectURL(blob) + '">');
var Confirmer = function(el) {
var self = this;
self.el = el;
self.$el = $(el);
if (self.el.tagName === 'INPUT') {
self.$el.blur(function() {
self.handleBlur();
});
} else if (self.el.tagName === 'SELECT') {
self.$el.focusout(function() {
self.handleBlur();
self.$el.change(function() {
self.$el.trigger('focusout');
})
});
}
self.$el.keypress(function(event) {
if (event.which !== 13) return; // We're only handling the enter key
this.tagName === 'INPUT' ? self.$el.trigger('blur') : self.$el.trigger('focusout');
});
self.$el
.on('validate', self.validate.bind(self))
.on('markValid', self.markValid.bind(self))
.on('markInvalid', self.markInvalid.bind(self));
};
Confirmer.prototype = {
// Check if input is valid, mark the field accordingly
handleBlur: function() {
this.validate();
var inputs = this.$el.parents('form').find('input, select');
inputs.each(function() {
if ($(this).val()) $(this).trigger('validate');
});
},
// Apply class '.invalid-input' to this element, animate a little x in from the right
markInvalid: function() {
var self = this;
self.$el.removeClass('valid-input').addClass('invalid-input');
var marker = self.$el.next('.valid-marker');
if (marker.length) {
if (marker.find('i').hasClass('fa-times')) return;
marker.stop().animate({right: self.offset, opacity: 0}, 150, function() {
$(this).find('i').removeClass('fa-check').addClass('fa-times');
$(this).animate({right: self.offset + self.distance, opacity: 1}, 150);
});
} else {
self.createMarker();
}
},
// Apply class '.valid-input' to this element, animate a little checkmark in from the right
markValid: function() {
var self = this;
self.$el.removeClass('invalid-input').addClass('valid-input');
var marker = self.$el.next('.valid-marker');
if (marker.length) {
if (marker.find('i').hasClass('fa-check')) return;
marker.stop().animate({right: self.offset, opacity: 0}, 150, function() {
$(this).find('i').removeClass('fa-times').addClass('fa-check');
$(this).animate({right: self.offset + self.distance, opacity: 1}, 150);
});
} else {
self.createMarker(true);
}
},
createMarker: function(valid) {
if (this.el.tagName === 'SELECT' && !config.overrideSelects) return;
var marker = $('<span class="valid-marker"><i class="fa fa-' + (valid ? 'check' : 'times') + '"></i></span>')
.css({
height: this.$el.outerHeight(),
top: (this.$el.offset().top - this.$el.parent().offset().top) + 'px'
});
this.$el.css({appearance: 'none', WebkitAppearance: 'none', MozAppearance: 'none'});
this.$el.after(marker).parent().css('position', 'relative');
marker.animate({right: this.offset + this.distance, opacity: 1}, 150);
},
isValid: function() {
var val = this.$el.val(),
name = this.$el.prop('name').toLowerCase(),
valid = name.indexOf('zip') > -1 ? config.validation.zip(val)
: name.indexOf('cvv') > -1 ? config.validation.cvv(val)
: name.indexOf('phone') > -1 ? config.validation.phone(val)
: name.indexOf('card') > -1 ? config.validation.creditcard(val)
: name.indexOf('email') > -1 ? config.validation.email(val)
: val && val.length;
return valid && val && val.length;
},
validate: function() {
this.isValid() ? this.markValid() : this.markInvalid();
},
get distance() {
return config.animationDistance;
},
get offset() {
var offsetLeft = this.$el.offset().left - this.$el.parent().offset().left,
offsetRight = this.$el.parent().outerWidth() - (offsetLeft + this.$el.outerWidth());
return offsetRight - 16 - config.offset;
}
};
// Create a new Confirmer object for every object selected by config.selector
$(config.selector).each(function() {
var confirmer = new Confirmer(this);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment