Skip to content

Instantly share code, notes, and snippets.

@cimmanon
Created April 4, 2014 12:36
Show Gist options
  • Save cimmanon/9973839 to your computer and use it in GitHub Desktop.
Save cimmanon/9973839 to your computer and use it in GitHub Desktop.
function addSubformListeners(el, isSubform) {
el.addEventListener("click", addListenerIfSubform, false); // bubbling
var useFieldsetFallback = false;
// Firefox allows disabled form elements to intercept click events, but does not allow them to bubble,
// so we have to inject some special elements to sit on top and intercept the click events
if (!hasEventsWhenDisabled()) {
addOverlays();
}
if (typeof document.createElement('fieldset').disabled === 'undefined') { // browser doesn't support disabling fieldsets
useFieldsetFallback = true;
var inputs = el.querySelectorAll('fieldset[disabled] input, fieldset[disabled] textarea, fieldset[disabled] select');
for (var i = 0, len = inputs.length; i < len; i++) {
if (!inputs[i].getAttribute('data-control')) {
inputs[i].disabled = true;
}
}
}
function addListenerIfSubform(ev) {
var subform;
// subformOverlay check here for FF
if (ev.target.className == 'subformOverlay') {
subform = ev.target.nextSibling;
} else {
subform = findAncestor(ev.target, isSubform);
}
if (subform) {
subformSetup(subform);
// call the event
if (document.createEvent) {
var evt = document.createEvent('Event');
evt.initEvent("click", true, true);
subform.dispatchEvent(evt);
} else { // old IE model
subform.fireEvent('onclick');
}
}
}
function addOverlays() {
var fieldsets = document.querySelectorAll('fieldset[disabled]');
var div = document.createElement('div');
div.className = 'subformOverlay';
for (var i = 0, len = fieldsets.length; i < len; i++) {
var d = div.cloneNode(true);
var f = fieldsets[i];
var p = f.parentNode;
p.insertBefore(d, f);
d.style.width = window.getComputedStyle(f).getPropertyValue('width') || f.currentStyle.width;
d.style.height = window.getComputedStyle(f).getPropertyValue('height') || f.currentStyle.height;
}
}
}
function subformSetup(form) {
form.addEventListener('click', clickEvents, false);
form.addEventListener('change', changeEvents, false);
form.addEventListener('change', setAsModified, false); // IE9 doesn't think onChange counts as onInput
form.addEventListener('input', setAsModified, false);
var useFieldsetFallback = typeof form.disabled === 'undefined';
var legend = form.getElementsByTagName('legend')[0]; // the primary legend for this subform
// toggle to display the options
var optionsToggle = legend.querySelector('input[type="checkbox"][data-control]');
// subforms if they exist for a toggle form
var subforms = form.getElementsByTagName('fieldset');
var fields = form.querySelectorAll('input, textarea, select');
enableForm();
function changeEvents(ev) {
switch (ev.target.type) { // toggleform togglers
case 'radio':
for (var i = 0, len = subforms.length; i < len; i++) {
if (subforms[i].attributes['name'].value == ev.target.value) {
enableFieldset(subforms[i]);
var thisLegend = subforms[i].getElementsByTagName('legend')[0];
legend.firstChild.nodeValue = thisLegend.innerText || thisLegend.textContent;
} else {
disableFieldset(subforms[i]);
form.className = form.className.replace(subforms[i].attributes['name'].value, ev.target.value);
}
if (useFieldsetFallback) {
fieldsetDisableFallback(fields);
}
}
break;
case 'checkbox':
if (form.getAttribute('disabled')) {
openOptions();
} else {
ev.target.checked ? openOptions() : closeOptions();
}
break;
}
}
function clickEvents(ev) {
switch (ev.target.tagName.toLowerCase()) {
case 'legend':
if (ev.target == legend) {
toggleForm();
}
break;
case 'input':
switch (ev.target.type) {
case 'button': // cancel/confirmation buttons
switch (ev.target.name) {
case 'cancel':
disableForm();
ev.preventDefault;
break;
case 'confirm':
closeOptions();
ev.preventDefault;
break;
default:
break;
}
break;
default:
break;
}
break;
case 'label':
if ((ev.target.getAttribute('for') == optionsToggle.id || ev.target.querySelector('input') == optionsToggle) && form.getAttribute('disabled')) {
enableForm();
}
break;
case 'fieldset':
if (ev.target == form) { // should only fire when setting up event delegation on the base subform
toggleLegend();
enableForm();
}
break;
default:
// do nothing
break;
}
if (ev.stopPropagation) {
ev.stopPropagation();
} else if (window.event) { // when invoking an event via dispatchEvent, window.event will be null in IE9
window.event.cancelBubble = true;
}
}
function toggleLegend() {
if (legend.getAttribute('data-label')) {
var oldLabel = legend.innerText || legend.textContent;
legend.firstChild.nodeValue = legend.getAttribute('data-label');
legend.setAttribute('data-label', oldLabel);
}
}
function toggleForm() {
toggleLegend();
form.getAttribute('disabled') ? enableForm() : disableForm();
}
function fieldsetDisableFallback(fields) {
for (var i = 0, len = fields.length; i < len; i++) {
var isDisabled = findAncestor(fields[i], function(el) {
return el.tagName.toLowerCase() == 'fieldset' && el.getAttribute('disabled');
});
fields[i].disabled = isDisabled ? true : false; // if the closest ancestor fieldset is disabled, disable it. otherwise enable it
}
}
function enableFieldset(fieldset) {
if (useFieldsetFallback) {
fieldset.removeAttribute('disabled');
} else {
fieldset.disabled = false;
}
}
function disableFieldset(fieldset) {
if (useFieldsetFallback) {
fieldset.setAttribute('disabled', 'disabled');
} else {
fieldset.disabled = true;
}
}
function enableForm() {
enableFieldset(form);
if (useFieldsetFallback) {
fieldsetDisableFallback(fields);
}
if (optionsToggle && optionsToggle.checked || !optionsToggle) {
openOptions();
}
}
function disableForm() {
disableFieldset(form);
if (useFieldsetFallback) {
fieldsetDisableFallback(fields);
}
// remove event listeners
form.removeEventListener('click', clickEvents, false);
form.removeEventListener('change', changeEvents, false);
form.removeEventListener('change', setAsModified, false);
form.removeEventListener('input', setAsModified, false);
closeOptions();
}
function toggleOptions() {
optionsToggle.checked ? closeOptions() : openOptions();
}
function closeOptions() {
if (optionsToggle && optionsToggle.checked) {
optionsToggle.checked = false;
}
form.className = form.className.replace(/\bopened\b/g, '');
}
function openOptions() {
if (optionsToggle && !optionsToggle.checked) {
optionsToggle.checked = true;
}
if (!hasClass('opened')(form)) {
form.className += (form.className.length == 0 ? '' : ' ') + ' opened';
}
}
function setAsModified(ev) {
// onChange events do not bubble :-(
if (!formIsModified(fields, isSubformControl)) {
form.className = form.className.replace(/\bmodified\b/g, '');
} else if (!hasClass('modified')(form)) {
form.className += (form.className.length == 0 ? '' : ' ') + 'modified';
}
}
}
// ====================================================================================================
// | Helper Functions
// ====================================================================================================
function isFieldset(el) {
return el.tagName.toLowerCase() == 'fieldset';
}
function isTag(tagName) {
return function(el) {
return el.tagName.toLowerCase() == tagName;
}
}
function hasClass(className) {
return function(el) {
return el.className ? el.className.split(' ').indexOf(className) !== -1 : false;
}
}
function findAncestor(el, equalityChecker) {
if (!el.parentNode || el == document.documentElement) { // if we're at the root of the document, there's nowhere else to go
return false;
}
var p = el.parentNode;
if (equalityChecker(p)) {
return p;
} else {
return findAncestor(p, equalityChecker);
}
}
function isSubformControl(field) {
return !!field.getAttribute('data-control');
}
function formIsModified(fields, isIgnoredField) {
for (var i = 0, len = fields.length; i < len; i++) {
var field = fields[i];
if (isIgnoredField(field) || field.disabled) {
continue;
}
if (field.tagName.toLowerCase() == 'select') {
if (!field.options[field.selectedIndex].defaultSelected) {
return true;
}
} else if ((field.type == 'radio' || field.type == 'checkbox') && typeof field.defaultChecked !== 'undefined') {
if (field.defaultChecked != field.checked) {
return true;
}
} else if (
(typeof field.defaultValue === 'undefined' && field.value != '') ||
(typeof field.defaultValue !== 'undefined' && field.value != field.defaultValue)) {
return true;
}
}
return false;
}
function resetFields(fields, isIgnoredField) {
for (var i = 0, len = fields.length; i < len; i++) {
var field = fields[i];
if ((typeof isIgnoredField === 'function' && isIgnoredField(field)) || field.disabled) {
continue;
}
if (field.tagName.toLowerCase() == 'select') {
var options = field.options;
for (var j = 0, len = options.length; j < len; j++) {
if (options[j].defaultSelected) {
field.selectedIndex = j;
continue;
}
}
field.selectedIndex = 0;
} else if ((field.type == 'radio' || field.type == 'checkbox') && typeof field.defaultChecked !== 'undefined') {
field.checked = field.defaultChecked;
} else if (typeof field.defaultValue !== 'undefined' && field.value != field.defaultValue) {
field.value = field.defaultValue;
}
}
}
function hasEventsWhenDisabled() {
var input = document.createElement('input');
input.disabled = true;
var e = document.createEvent('HTMLEvents');
e.initEvent('click', true, true);
try {
input.dispatchEvent(e);
return true;
} catch(err) {}
return false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment