453 lines
13 KiB
JavaScript
453 lines
13 KiB
JavaScript
![]() |
/*!
|
||
|
* JavaScript Custom Forms
|
||
|
*
|
||
|
* Copyright 2014-2015 PSD2HTML - http://psd2html.com/jcf
|
||
|
* Released under the MIT license (LICENSE.txt)
|
||
|
*
|
||
|
* Version: 1.2.1
|
||
|
*/
|
||
|
;(function(root, factory) {
|
||
|
'use strict';
|
||
|
if (typeof define === 'function' && define.amd) {
|
||
|
define(['jquery'], factory);
|
||
|
} else if (typeof exports === 'object') {
|
||
|
module.exports = factory(require('jquery'));
|
||
|
} else {
|
||
|
root.jcf = factory(jQuery);
|
||
|
}
|
||
|
}(this, function($) {
|
||
|
'use strict';
|
||
|
|
||
|
// define version
|
||
|
var version = '1.2.1';
|
||
|
|
||
|
// private variables
|
||
|
var customInstances = [];
|
||
|
|
||
|
// default global options
|
||
|
var commonOptions = {
|
||
|
optionsKey: 'jcf',
|
||
|
dataKey: 'jcf-instance',
|
||
|
rtlClass: 'jcf-rtl',
|
||
|
focusClass: 'jcf-focus',
|
||
|
pressedClass: 'jcf-pressed',
|
||
|
disabledClass: 'jcf-disabled',
|
||
|
hiddenClass: 'jcf-hidden',
|
||
|
resetAppearanceClass: 'jcf-reset-appearance',
|
||
|
unselectableClass: 'jcf-unselectable'
|
||
|
};
|
||
|
|
||
|
// detect device type
|
||
|
var isTouchDevice = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch,
|
||
|
isWinPhoneDevice = /Windows Phone/.test(navigator.userAgent);
|
||
|
commonOptions.isMobileDevice = !!(isTouchDevice || isWinPhoneDevice);
|
||
|
|
||
|
// create global stylesheet if custom forms are used
|
||
|
var createStyleSheet = function() {
|
||
|
var styleTag = $('<style>').appendTo('head'),
|
||
|
styleSheet = styleTag.prop('sheet') || styleTag.prop('styleSheet');
|
||
|
|
||
|
// crossbrowser style handling
|
||
|
var addCSSRule = function(selector, rules, index) {
|
||
|
if (styleSheet.insertRule) {
|
||
|
styleSheet.insertRule(selector + '{' + rules + '}', index);
|
||
|
} else {
|
||
|
styleSheet.addRule(selector, rules, index);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// add special rules
|
||
|
addCSSRule('.' + commonOptions.hiddenClass, 'position:absolute !important;left:-9999px !important;height:1px !important;width:1px !important;margin:0 !important;border-width:0 !important;-webkit-appearance:none;-moz-appearance:none;appearance:none');
|
||
|
addCSSRule('.' + commonOptions.rtlClass + ' .' + commonOptions.hiddenClass, 'right:-9999px !important; left: auto !important');
|
||
|
addCSSRule('.' + commonOptions.unselectableClass, '-webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; -webkit-tap-highlight-color: rgba(0,0,0,0);');
|
||
|
addCSSRule('.' + commonOptions.resetAppearanceClass, 'background: none; border: none; -webkit-appearance: none; appearance: none; opacity: 0; filter: alpha(opacity=0);');
|
||
|
|
||
|
// detect rtl pages
|
||
|
var html = $('html'), body = $('body');
|
||
|
if (html.css('direction') === 'rtl' || body.css('direction') === 'rtl') {
|
||
|
html.addClass(commonOptions.rtlClass);
|
||
|
}
|
||
|
|
||
|
// handle form reset event
|
||
|
html.on('reset', function() {
|
||
|
setTimeout(function() {
|
||
|
api.refreshAll();
|
||
|
}, 0);
|
||
|
});
|
||
|
|
||
|
// mark stylesheet as created
|
||
|
commonOptions.styleSheetCreated = true;
|
||
|
};
|
||
|
|
||
|
// simplified pointer events handler
|
||
|
(function() {
|
||
|
var pointerEventsSupported = navigator.pointerEnabled || navigator.msPointerEnabled,
|
||
|
touchEventsSupported = ('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch,
|
||
|
eventList, eventMap = {}, eventPrefix = 'jcf-';
|
||
|
|
||
|
// detect events to attach
|
||
|
if (pointerEventsSupported) {
|
||
|
eventList = {
|
||
|
pointerover: navigator.pointerEnabled ? 'pointerover' : 'MSPointerOver',
|
||
|
pointerdown: navigator.pointerEnabled ? 'pointerdown' : 'MSPointerDown',
|
||
|
pointermove: navigator.pointerEnabled ? 'pointermove' : 'MSPointerMove',
|
||
|
pointerup: navigator.pointerEnabled ? 'pointerup' : 'MSPointerUp'
|
||
|
};
|
||
|
} else {
|
||
|
eventList = {
|
||
|
pointerover: 'mouseover',
|
||
|
pointerdown: 'mousedown' + (touchEventsSupported ? ' touchstart' : ''),
|
||
|
pointermove: 'mousemove' + (touchEventsSupported ? ' touchmove' : ''),
|
||
|
pointerup: 'mouseup' + (touchEventsSupported ? ' touchend' : '')
|
||
|
};
|
||
|
}
|
||
|
|
||
|
// create event map
|
||
|
$.each(eventList, function(targetEventName, fakeEventList) {
|
||
|
$.each(fakeEventList.split(' '), function(index, fakeEventName) {
|
||
|
eventMap[fakeEventName] = targetEventName;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// jQuery event hooks
|
||
|
$.each(eventList, function(eventName, eventHandlers) {
|
||
|
eventHandlers = eventHandlers.split(' ');
|
||
|
$.event.special[eventPrefix + eventName] = {
|
||
|
setup: function() {
|
||
|
var self = this;
|
||
|
$.each(eventHandlers, function(index, fallbackEvent) {
|
||
|
if (self.addEventListener) self.addEventListener(fallbackEvent, fixEvent, false);
|
||
|
else self['on' + fallbackEvent] = fixEvent;
|
||
|
});
|
||
|
},
|
||
|
teardown: function() {
|
||
|
var self = this;
|
||
|
$.each(eventHandlers, function(index, fallbackEvent) {
|
||
|
if (self.addEventListener) self.removeEventListener(fallbackEvent, fixEvent, false);
|
||
|
else self['on' + fallbackEvent] = null;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
});
|
||
|
|
||
|
// check that mouse event are not simulated by mobile browsers
|
||
|
var lastTouch = null;
|
||
|
var mouseEventSimulated = function(e) {
|
||
|
var dx = Math.abs(e.pageX - lastTouch.x),
|
||
|
dy = Math.abs(e.pageY - lastTouch.y),
|
||
|
rangeDistance = 25;
|
||
|
|
||
|
if (dx <= rangeDistance && dy <= rangeDistance) {
|
||
|
return true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// normalize event
|
||
|
var fixEvent = function(e) {
|
||
|
var origEvent = e || window.event,
|
||
|
touchEventData = null,
|
||
|
targetEventName = eventMap[origEvent.type];
|
||
|
|
||
|
e = $.event.fix(origEvent);
|
||
|
e.type = eventPrefix + targetEventName;
|
||
|
|
||
|
if (origEvent.pointerType) {
|
||
|
switch (origEvent.pointerType) {
|
||
|
case 2: e.pointerType = 'touch'; break;
|
||
|
case 3: e.pointerType = 'pen'; break;
|
||
|
case 4: e.pointerType = 'mouse'; break;
|
||
|
default: e.pointerType = origEvent.pointerType;
|
||
|
}
|
||
|
} else {
|
||
|
e.pointerType = origEvent.type.substr(0, 5); // "mouse" or "touch" word length
|
||
|
}
|
||
|
|
||
|
if (!e.pageX && !e.pageY) {
|
||
|
touchEventData = origEvent.changedTouches ? origEvent.changedTouches[0] : origEvent;
|
||
|
e.pageX = touchEventData.pageX;
|
||
|
e.pageY = touchEventData.pageY;
|
||
|
}
|
||
|
|
||
|
if (origEvent.type === 'touchend') {
|
||
|
lastTouch = { x: e.pageX, y: e.pageY };
|
||
|
}
|
||
|
if (e.pointerType === 'mouse' && lastTouch && mouseEventSimulated(e)) {
|
||
|
return;
|
||
|
} else {
|
||
|
return ($.event.dispatch || $.event.handle).call(this, e);
|
||
|
}
|
||
|
};
|
||
|
}());
|
||
|
|
||
|
// custom mousewheel/trackpad handler
|
||
|
(function() {
|
||
|
var wheelEvents = ('onwheel' in document || document.documentMode >= 9 ? 'wheel' : 'mousewheel DOMMouseScroll').split(' '),
|
||
|
shimEventName = 'jcf-mousewheel';
|
||
|
|
||
|
$.event.special[shimEventName] = {
|
||
|
setup: function() {
|
||
|
var self = this;
|
||
|
$.each(wheelEvents, function(index, fallbackEvent) {
|
||
|
if (self.addEventListener) self.addEventListener(fallbackEvent, fixEvent, false);
|
||
|
else self['on' + fallbackEvent] = fixEvent;
|
||
|
});
|
||
|
},
|
||
|
teardown: function() {
|
||
|
var self = this;
|
||
|
$.each(wheelEvents, function(index, fallbackEvent) {
|
||
|
if (self.addEventListener) self.removeEventListener(fallbackEvent, fixEvent, false);
|
||
|
else self['on' + fallbackEvent] = null;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var fixEvent = function(e) {
|
||
|
var origEvent = e || window.event;
|
||
|
e = $.event.fix(origEvent);
|
||
|
e.type = shimEventName;
|
||
|
|
||
|
// old wheel events handler
|
||
|
if ('detail' in origEvent) { e.deltaY = -origEvent.detail; }
|
||
|
if ('wheelDelta' in origEvent) { e.deltaY = -origEvent.wheelDelta; }
|
||
|
if ('wheelDeltaY' in origEvent) { e.deltaY = -origEvent.wheelDeltaY; }
|
||
|
if ('wheelDeltaX' in origEvent) { e.deltaX = -origEvent.wheelDeltaX; }
|
||
|
|
||
|
// modern wheel event handler
|
||
|
if ('deltaY' in origEvent) {
|
||
|
e.deltaY = origEvent.deltaY;
|
||
|
}
|
||
|
if ('deltaX' in origEvent) {
|
||
|
e.deltaX = origEvent.deltaX;
|
||
|
}
|
||
|
|
||
|
// handle deltaMode for mouse wheel
|
||
|
e.delta = e.deltaY || e.deltaX;
|
||
|
if (origEvent.deltaMode === 1) {
|
||
|
var lineHeight = 16;
|
||
|
e.delta *= lineHeight;
|
||
|
e.deltaY *= lineHeight;
|
||
|
e.deltaX *= lineHeight;
|
||
|
}
|
||
|
|
||
|
return ($.event.dispatch || $.event.handle).call(this, e);
|
||
|
};
|
||
|
}());
|
||
|
|
||
|
// extra module methods
|
||
|
var moduleMixin = {
|
||
|
// provide function for firing native events
|
||
|
fireNativeEvent: function(elements, eventName) {
|
||
|
$(elements).each(function() {
|
||
|
var element = this, eventObject;
|
||
|
if (element.dispatchEvent) {
|
||
|
eventObject = document.createEvent('HTMLEvents');
|
||
|
eventObject.initEvent(eventName, true, true);
|
||
|
element.dispatchEvent(eventObject);
|
||
|
} else if (document.createEventObject) {
|
||
|
eventObject = document.createEventObject();
|
||
|
eventObject.target = element;
|
||
|
element.fireEvent('on' + eventName, eventObject);
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
// bind event handlers for module instance (functions beggining with "on")
|
||
|
bindHandlers: function() {
|
||
|
var self = this;
|
||
|
$.each(self, function(propName, propValue) {
|
||
|
if (propName.indexOf('on') === 0 && $.isFunction(propValue)) {
|
||
|
// dont use $.proxy here because it doesn't create unique handler
|
||
|
self[propName] = function() {
|
||
|
return propValue.apply(self, arguments);
|
||
|
};
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// public API
|
||
|
var api = {
|
||
|
version: version,
|
||
|
modules: {},
|
||
|
getOptions: function() {
|
||
|
return $.extend({}, commonOptions);
|
||
|
},
|
||
|
setOptions: function(moduleName, moduleOptions) {
|
||
|
if (arguments.length > 1) {
|
||
|
// set module options
|
||
|
if (this.modules[moduleName]) {
|
||
|
$.extend(this.modules[moduleName].prototype.options, moduleOptions);
|
||
|
}
|
||
|
} else {
|
||
|
// set common options
|
||
|
$.extend(commonOptions, moduleName);
|
||
|
}
|
||
|
},
|
||
|
addModule: function(proto) {
|
||
|
// proto is factory function
|
||
|
if ($.isFunction(proto)) {
|
||
|
proto = proto($, window);
|
||
|
}
|
||
|
|
||
|
// add module to list
|
||
|
var Module = function(options) {
|
||
|
// save instance to collection
|
||
|
if (!options.element.data(commonOptions.dataKey)) {
|
||
|
options.element.data(commonOptions.dataKey, this);
|
||
|
}
|
||
|
customInstances.push(this);
|
||
|
|
||
|
// save options
|
||
|
this.options = $.extend({}, commonOptions, this.options, getInlineOptions(options.element), options);
|
||
|
|
||
|
// bind event handlers to instance
|
||
|
this.bindHandlers();
|
||
|
|
||
|
// call constructor
|
||
|
this.init.apply(this, arguments);
|
||
|
};
|
||
|
|
||
|
// parse options from HTML attribute
|
||
|
var getInlineOptions = function(element) {
|
||
|
var dataOptions = element.data(commonOptions.optionsKey),
|
||
|
attrOptions = element.attr(commonOptions.optionsKey);
|
||
|
|
||
|
if (dataOptions) {
|
||
|
return dataOptions;
|
||
|
} else if (attrOptions) {
|
||
|
try {
|
||
|
return $.parseJSON(attrOptions);
|
||
|
} catch (e) {
|
||
|
// ignore invalid attributes
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// set proto as prototype for new module
|
||
|
Module.prototype = proto;
|
||
|
|
||
|
// add mixin methods to module proto
|
||
|
$.extend(proto, moduleMixin);
|
||
|
if (proto.plugins) {
|
||
|
$.each(proto.plugins, function(pluginName, plugin) {
|
||
|
$.extend(plugin.prototype, moduleMixin);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// override destroy method
|
||
|
var originalDestroy = Module.prototype.destroy;
|
||
|
Module.prototype.destroy = function() {
|
||
|
this.options.element.removeData(this.options.dataKey);
|
||
|
|
||
|
for (var i = customInstances.length - 1; i >= 0; i--) {
|
||
|
if (customInstances[i] === this) {
|
||
|
customInstances.splice(i, 1);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (originalDestroy) {
|
||
|
originalDestroy.apply(this, arguments);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// save module to list
|
||
|
this.modules[proto.name] = Module;
|
||
|
},
|
||
|
getInstance: function(element) {
|
||
|
return $(element).data(commonOptions.dataKey);
|
||
|
},
|
||
|
replace: function(elements, moduleName, customOptions) {
|
||
|
var self = this,
|
||
|
instance;
|
||
|
|
||
|
if (!commonOptions.styleSheetCreated) {
|
||
|
createStyleSheet();
|
||
|
}
|
||
|
|
||
|
$(elements).each(function() {
|
||
|
var moduleOptions,
|
||
|
element = $(this);
|
||
|
|
||
|
instance = element.data(commonOptions.dataKey);
|
||
|
if (instance) {
|
||
|
instance.refresh();
|
||
|
} else {
|
||
|
if (!moduleName) {
|
||
|
$.each(self.modules, function(currentModuleName, module) {
|
||
|
if (module.prototype.matchElement.call(module.prototype, element)) {
|
||
|
moduleName = currentModuleName;
|
||
|
return false;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
if (moduleName) {
|
||
|
moduleOptions = $.extend({ element: element }, customOptions);
|
||
|
instance = new self.modules[moduleName](moduleOptions);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return instance;
|
||
|
},
|
||
|
refresh: function(elements) {
|
||
|
$(elements).each(function() {
|
||
|
var instance = $(this).data(commonOptions.dataKey);
|
||
|
if (instance) {
|
||
|
instance.refresh();
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
destroy: function(elements) {
|
||
|
$(elements).each(function() {
|
||
|
var instance = $(this).data(commonOptions.dataKey);
|
||
|
if (instance) {
|
||
|
instance.destroy();
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
replaceAll: function(context) {
|
||
|
var self = this;
|
||
|
$.each(this.modules, function(moduleName, module) {
|
||
|
$(module.prototype.selector, context).each(function() {
|
||
|
if (this.className.indexOf('jcf-ignore') < 0) {
|
||
|
self.replace(this, moduleName);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
},
|
||
|
refreshAll: function(context) {
|
||
|
if (context) {
|
||
|
$.each(this.modules, function(moduleName, module) {
|
||
|
$(module.prototype.selector, context).each(function() {
|
||
|
var instance = $(this).data(commonOptions.dataKey);
|
||
|
if (instance) {
|
||
|
instance.refresh();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
} else {
|
||
|
for (var i = customInstances.length - 1; i >= 0; i--) {
|
||
|
customInstances[i].refresh();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
destroyAll: function(context) {
|
||
|
if (context) {
|
||
|
$.each(this.modules, function(moduleName, module) {
|
||
|
$(module.prototype.selector, context).each(function(index, element) {
|
||
|
var instance = $(element).data(commonOptions.dataKey);
|
||
|
if (instance) {
|
||
|
instance.destroy();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
} else {
|
||
|
while (customInstances.length) {
|
||
|
customInstances[0].destroy();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return api;
|
||
|
}));
|