Interactive Disabled Input
About
This demo shows a cross-browser pattern for giving users feedback when they click disabled form controls, including a shake animation and disabled cursor behavior. is not included.
Demonstration
Try clicking or tapping the disabled fields below. You should see a not-allowed cursor over disabled targets on hover and a brief shake animation on click or touch.
Status: Ready
Code
Javascript
disabled_shake.js
// events.js
function addEvent(evnt, elem, func) {
if (elem.addEventListener) { // W3C DOM
elem.addEventListener(evnt, func, false);
}
else if (elem.attachEvent) { // IE DOM
elem.attachEvent('on' + evnt, func);
}
else { // No much to do
elem['on' + evnt] = func;
}
}
// classes.js
/**
* Returns whether the given element has the given class.
*
* @param {Element} element
* @param {string} className
* @returns {boolean}
*/
function hasClass(element, className) {
className = " " + className + " ";
return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(className) > -1
}
function addClass(elements, className) {
if ( elements.length === undefined ) elements = [elements];
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (!hasClass(element, className)) {
if (element.classList) {
element.classList.add(className);
} else {
element.className = ( element.className + ' ' + className ).replace( /\s+/g, ' ');
}
}
}
}
function removeClass(elements, className) {
if ( elements.length === undefined ) elements = [ elements ];
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (hasClass(element, className)) {
if (element.classList) {
element.classList.remove(className);
} else {
element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
}
}
}
// throttle.js
var throttleGateKeys = {
mousemove: false,
click: false
};
var throttleGateTimes = {
mousemove: 0,
click: 0
};
var throttleGateDebounceEvents = {
mousemove: false,
scroll: false,
click: false
};
var throttleGateIntervals = {
mousemove: 100,
click: 200 // 200ms is disabled animation duration
};
var throttleGateDebounceIntervalMin = 10;
function throttleGate(affectedKey, intervalKey) {
// function throttleGate(affectedKey, forceGate) {
if (throttleGateKeys[intervalKey]) return true;
// if (throttleGateKeys[affectedKey] || forceGate) return true;
throttleGateKeys[affectedKey] = true;
setTimeout(function() {
throttleGateKeys[affectedKey] = false;
}, throttleGateIntervals[affectedKey]);
throttleGateTimes[affectedKey] = (new Date()).getTime(); // for debounce timing
return false;
}
function debounceThrottleGate(key, debounceKey, func, event) {
var intervalKey = key;
if (key != 'click') {
if(throttleGateDebounceEvents['click']) {
// if within current click debounce time and inside last clicked rectangle.
// do not set interval key, instead call true in the throttle gate anyways (forceGate parameter)
intervalKey = 'click';
}
}
if (throttleGate(key, intervalKey)) {
var throttleInterval = throttleGateIntervals[intervalKey];
clearTimeout(throttleGateDebounceEvents[debounceKey]);
throttleGateDebounceEvents[debounceKey] = setTimeout(function() {
if (event) {
func(event);
}
else {
func();
}
}, Math.min(Math.max(throttleInterval - ((new Date()).getTime() - throttleGateTimes[intervalKey]), 0), throttleInterval) + throttleGateDebounceIntervalMin);
return true;
}
return false;
}
// disabled_shake.js
var root = document.documentElement;
var lastMouseX = 0;
var lastMouseY = 0;
function updateMouseCoords(coordX, coordY) {
lastMouseX = coordX;
lastMouseY = coordY;
}
function isMouseCoordsInElem(elem) {
var rect = elem.getBoundingClientRect();
return parseInt(rect.left, 10) < lastMouseX && lastMouseX < parseInt(rect.left + rect.width, 10) && parseInt(rect.top, 10) < lastMouseY && lastMouseY < parseInt(rect.top + rect.height, 10);
}
var topLevelElemOverlapped;
function getTopLevelElemOverlapped(elem) {
var isWithin = isMouseCoordsInElem(elem);
if (isWithin) {
topLevelElemOverlapped = elem;
var hasDecendants = elem.hasChildNodes();
if (hasDecendants) {
var decendants = elem.children;
var len = decendants.length;
for (var i = 0; i < len; i++) {
getTopLevelElemOverlapped(decendants[i]);
}
}
}
}
function isElementDisabled(element) {
return element && (element.disabled || hasClass(element, 'disabled'));
}
function tryDisabledClickRejection(elem) {
if (isElementDisabled(elem)) {
addClass(elem, 'show-disabled-animation');
setTimeout(function() {
removeClass(elem, 'show-disabled-animation');
}, throttleGateIntervals['click']);
}
}
function tryClick(event) {
if(debounceThrottleGate('click', 'click', tryClick, event)) return;
updateMouseCoords(event.x, event.y);
getTopLevelElemOverlapped(root);
if (topLevelElemOverlapped && isElementDisabled(topLevelElemOverlapped)) {
tryDisabledClickRejection(topLevelElemOverlapped.parentNode);
var elems = topLevelElemOverlapped.parentNode.children;
var len = elems.length;
for (var i = 0; i < len; i++) {
var elem = elems[i];
tryDisabledClickRejection(elem);
}
}
}
function tryDisabledCursor() {
getTopLevelElemOverlapped(root);
if( isElementDisabled(topLevelElemOverlapped) ) {
addClass(root, 'show-disabled-cursor');
}
else {
removeClass(root, 'show-disabled-cursor');
}
}
function tryScroll() {
if(debounceThrottleGate('mousemove', 'scroll', tryScroll, false)) return;
tryDisabledCursor();
}
function tryMousemove(event) {
if(debounceThrottleGate('mousemove', 'mousemove', tryMousemove, event)) return;
updateMouseCoords(event.x, event.y);
tryDisabledCursor();
}
var areDisabledElements = false;
function initDisabledShake() {
if ( areDisabledElements ) return; // already running;
var inputs = document.querySelectorAll('input, textarea');
for ( var i = 0; i < inputs.length; i++ )
{
if ( inputs[i].disabled )
{
areDisabledElements = true;
break;
}
}
if ( areDisabledElements ) {
addEvent('click', document, function(event) {
// event.preventDefault(); // only if over disabled element || is click anim throttled?
tryClick(event);
});
addEvent('scroll', document, function() {
tryScroll();
});
addEvent('mousemove', document, function(event) {
tryMousemove(event);
});
}
}
initDisabledShake();CSS
disabled_shake.css
html.show-disabled-cursor {
cursor: not-allowed;
}
@keyframes disabledAnimation {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-$page_gap);
}
75% {
transform: translateX($page_gap);
}
100% {
transform: translateX(0);
}
}
.disabled, :not(.disabled) > :disabled {
display: inline-block;
pointer-events: none;
}
.show-disabled-animation {
animation-duration: 0.2s;
animation-iteration-count: 1;
animation-name: disabledAnimation;
}Attribution
This page is comprised of my own additions and either partially or heavily modified elements from the following source(s):