Skip to content

Instantly share code, notes, and snippets.

@leekiernan
Created November 12, 2014 12:42
Show Gist options
  • Save leekiernan/8b34196e6004f7f3129e to your computer and use it in GitHub Desktop.
Save leekiernan/8b34196e6004f7f3129e to your computer and use it in GitHub Desktop.
var Hook = {
hooks: [],
register: function ( name, callback ) {
if( 'undefined' == typeof( Hook.hooks[name] ) ) {
// Create empty array if first hook for name.
Hook.hooks[name] = []
}
Hook.hooks[name].push( callback )
},
call: function ( name, arguments ) {
if( 'undefined' != typeof( Hook.hooks[name] ) ) {
for( var i = 0; i < Hook.hooks[name].length; ++i ) {
if( true != Hook.hooks[name][i]( arguments ) ) { break; }
}
}
}
}
/* global Clone */
'use strict';
// var DEFAULT_MAX_CLONES = 3,
var DEFAULT_CLONE_CONTAINER = '.cloneable-container',
DEFAULT_CLONE_SUMMARY = '.cloneable-summary',
DEFAULT_CLONE_ACTIONS = '.cloneable-actions';
// Container object for handling multiple clones, order and creation/deletion.
// Manage headers, actions for adding, removing.
// container fieldset passed in. init break apart and store requirements.
function Cloneable(fieldset, maxClones) {
// structure of HTML.
this.cloneContainer = fieldset.find('> '+ DEFAULT_CLONE_CONTAINER);
this.summary = fieldset.find('> '+ DEFAULT_CLONE_SUMMARY);
this.actions = fieldset.find('> '+ DEFAULT_CLONE_ACTIONS);
this.master = this.cloneContainer.find('> fieldset.clone').detach();
this.clones = [];
this.cloneIterator = fieldset.data('iterator');
this.keepDefault = Boolean(fieldset.data('keep-default'));
this.confirmDelete = Boolean(fieldset.data('confirm-delete'));
this.maxClones = !!(maxClones) ? maxClones : -1;
this.displaySummary = false;
if( this.maxClones > -1 ) { this.actions.find('.clone-button').append( $('<small>' ).text('Max: '+ this.maxClones).css({ display:'block', lineHeight:1, fontSize:'0.66em' }) ); }
this.actions.find('.clone-button').on('click', { cloneable: this }, function(event) {
// Add click event. Only applies to outermost element,
// as this will not remain on anything that's cloned into the page
// later, ie nested fieldsets.
// var cloneable = event.data.cloneable;
//if(($("#form").parsley()).isValid()) {
submitClicked = true;
if(($("#form").parsley()).validate()) {
event.preventDefault();
event.data.cloneable.addClone();
($("#form").parsley()).destroy();
initParsley();
} else {
alert("ERROR!\n\nCannot add a new driver until the current driver details have been fully completed.");
}
});
// Create new element on initialisation; keep clean copy for duplication.
// master copy can contain information on how to rename ID/name fields,
// data-name="QuestionSomething-{{cloneID}}-N"
// Clones-in-clones: TODO.
this.addClone(true);
return this;
}
Cloneable.prototype.find = function(clone) {
var result = $.grep( this.clones, function(e) { return (e === clone) ? e : false; });
return result[0];
};
Cloneable.prototype.findIndex = function(clone) {
return this.clones.indexOf( this.find( clone ) );
};
Cloneable.prototype.lastClone = function() {
return this.clones.slice(-1)[0];
};
Cloneable.prototype.setIDs = function() {
// Each clone.id = x
for( var i=0 ; i < this.clones.length ; i++ ) {
// ID as 1.. not 0..
this.clones[i].setID(i + 1);
}
};
Cloneable.prototype.checkMax = function() {
return this.clones.length < (this.maxClones);
};
Cloneable.prototype.append = function(clone) {
clone.fieldset.appendTo( this.cloneContainer );
return this;
};
// move to first in array.
Cloneable.prototype.setDefault = function(clone) {
var removeClone = this.clones.splice( this.findIndex(clone), 1 )[0];
// All remaining NOT default.
for( var i=0 ; i < this.clones.length ; i++ ) {
this.clones[i].isDefault = false;
}
// Default to first in array.
this.clones.unshift(removeClone);
removeClone.isDefault = true;
this.clearSummary().allHeaders().setIDs();
return this;
};
Cloneable.prototype.addClone = function(isDefault) {
// if( !this.checkMax() ) { alert('no!'); return false; }
var newClone = new Clone( this.master.clone(), this, {
iterator: this.cloneIterator,
isDefault: (!!isDefault ? isDefault : false),
keepDefault: this.keepDefault,
confirmDelete: this.confirmDelete
});
var lastClone = this.lastClone();
if( lastClone ) {
this.addHeader( lastClone.fold().createHeader() );
}
this.clones.push( newClone );
this.append(newClone).setIDs();
// inner cloneable elements now initialized as own.
newClone.fieldset.find('.cloneable').each( function(i, container) {
window.cloneableFieldsets[ container.getAttribute('id') ] = new Cloneable( $(container), $(container).data('clones') );
});
// $(document).trigger('cloneAdded', [this]);
return this;
};
Cloneable.prototype.removeClone = function(clone) {
this.clones.splice( this.findIndex(clone), 1 );
clone.destruct();
// $(document).trigger('cloneRemoved', [this]);
return this;
};
Cloneable.prototype.allHeaders = function(editLink) {
for( var i=0 ; i < this.clones.length; i++ ) {
var cl = this.clones[i];
// Don't display 'open' fieldset.
if( cl.folded ) {
this.clones[i].header = false;
this.addHeader( cl.destroyHeader().createHeader(editLink) );
}
}
return this;
};
Cloneable.prototype.clearSummary = function() {
this.summary.html('');
return this;
};
Cloneable.prototype.addHeader = function(header) {
this.summary.append(header);
window.scrollTo( this.summary.position().left, this.summary.position().top );
return this;
};
Cloneable.prototype.foldAll = function() {
for( var i=0 ; i < this.clones.length; i++ ) {
var cl = this.clones[i];
cl.fold();
}
return this;
};
Cloneable.prototype.serialize = function() {
var data = {};
for( var i=0 ; i<this.clones.length ; i++ ) {
data[i] = this.clones[i].serialize();
}
return data;
};
Cloneable.prototype.parse = function(parseHTML) {
// parseHTML.find('> '+ DEFAULT_CLONE_CONTAINER).replaceWith( this.cloneContainer);
// parseHTML.find('> '+ DEFAULT_CLONE_SUMMARY).replaceWith( this.summary );
// parseHTML.find('> '+ DEFAULT_CLONE_ACTIONS).replaceWith( this.actions);
};
// Cloneable.prototype.focus = function(cloneIndex) {
// this.foldAll().clones[cloneIndex].unfold();
// };
$(function() {
window.cloneableFieldsets = {};
$('.cloneable#driver').each( function(i, container) {
window.cloneableFieldsets[ container.getAttribute('id') ] = new Cloneable( $(container), $(container).data('clones') );
});
var driver, areClones;
driver = window.cloneableFieldsets.driver;
if( !!driver ) {
driver.actions.find('.clone-button').on('click', function() {
var nextButton, summaryButton;
summaryButton = $('<button>', { class: 'btn btn-lg btn-success pull-right col-xs-12 col-sm-6 col-lg-5' }).text('I\'ve finished adding drivers ▶');
nextButton = $(this).closest('form').find('#form-actions .btn-success');
if( !!areClones ) {
return true;
} else {
summaryButton.on('click', function(e) {
submitClicked = true;
if(($("#form").parsley()).validate()) {
e.preventDefault();
$(this).after(nextButton).detach();
driver.lastClone().fold();
driver.clearSummary().allHeaders(true);
areClones = false;
($("#form").parsley()).destroy();
initParsley();
} else {
alert("ERROR!\n\nCannot add a new driver until the current driver details have been fully completed.");
}
});
driver.clearSummary().allHeaders(false);
nextButton.after(summaryButton).detach();
areClones = true;
return true;
}
});
}
});
// Clone -> change IDs -> fold -> append.
// Change IDs -> re-order -> re-fold
/* global Hook */
'use strict';
// Container for clone of a fieldset, and associated information.
// Fieldset is jQuery clone of the whole fieldset - should be empty but we clear on init.
// master is a ref to the parent object - should always be Cloneable.
// iterator will help to identify the target for resetting names and ids on form fields. {driver}
// isDefault flag for primary/default clone.
function Clone(fieldset, master, options) {
if( !master ) { return false; }
this.fieldset = fieldset;
this.master = master;
this.iterator = options.iterator;
this.isDefault = (!!options.isDefault) ? options.isDefault : false;
this.keepDefault = (!!options.keepDefault) ? options.keepDefault : false;
this.confirmDelete = (!!options.confirmDelete) ? options.confirmDelete : false;
this.submitButtons = [];
this.closeButtons = [];
this.makeDefaultButtons = [];
this.id = 99;
this.folded = false;
// if( !this.closeButtons instanceof Array ) { this.closeButtons = [closeButtons]; }
// if( !this.makeDefaultButtons instanceof Array ) { this.makeDefaultButtons = [makeDefaultButtons]; }
// Ensure clean fieldset on initialization.
this.clear();
// If this fieldset has it's own actions, we
// replace with internal actions and remove the master
var actions = this.fieldset.find('> .clone-actions');
if( !!actions.length ) {
this.master.actions.hide();
var cancelButton = actions.find('.clone-remove'),
submitButton = actions.find('.clone-submit');
this.addRemoveButton(cancelButton);
this.addSubmitButton(submitButton);
}
this.fieldset.find('input.format-number').number( true );
Hook.call( 'cloneCreation', { self: this, fieldset: this.fieldset } );
return this;
}
// this.addFooter();
Clone.prototype.addFooter = function() { };
// this.removeFooter();
Clone.prototype.removeFooter = function() { };
// this.fold();
Clone.prototype.fold = function() {
this.fieldset.hide();
this.folded = true;
return this;
};
// this.unfold();
Clone.prototype.unfold = function() {
this.fieldset.show();
this.folded = false;
return this;
};
// this.clear();
Clone.prototype.clear = function() {
this.fieldset.find(':input')
.not(':button, :submit, :reset, :hidden')
.val('')
.removeAttr('checked')
.removeAttr('selected');
return this;
};
// this.addMakeDefaultButton(button);
Clone.prototype.addMakeDefaultButton = function(button) {
this.makeDefaultButtons.push(button);
button.click({ clone: this }, function(e) {
e.preventDefault();
var clone = e.data.clone;
clone.master.setDefault(clone);
} );
};
// this.addRemoveButton(button);
Clone.prototype.addRemoveButton = function(button) {
this.closeButtons.push(button);
var removeClone = function(clone) {
clone.master.removeClone(clone);
if( !clone.master.actions.is(':visible') ) {
clone.master.actions.show();
}
return true;
};
if( !!this.confirmDelete ) {
button.click({ clone: this }, function(e) {
e.preventDefault();
var clone = e.data.clone;
var modalInner = '<div class="modal-dialog"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button></div><div class="modal-body"><h3 class="modal-title">A word of warning...</h3><p>You are about to delete this driver.</p><p>Are you sure you want to continue?</p><div class="btn-group-lg"><span><button type="button" class="btn btn-lg btn-info" data-dismiss="modal">Close</button></span><span><button type="button" class="btn btn-lg btn-success confirm-delete">Yes</button></span></div></div></div></div>',
modalElement = $('<div>', { class: 'modal fade' } ).attr('id', 'confirmDelete').attr('tabindex', '-1').attr('role', 'dialog').attr('aria-labelledby', 'confirmDeleteLabel').attr('aria-hidden', 'true').html(modalInner);
$('body').append( modalElement );
modalElement.modal('show');
modalElement.on('hidden.bs.modal', function() {
modalElement.remove();
});
modalElement.find('.btn-success').on('click', function() {
removeClone(clone);
modalElement.modal('hide');
});
});
} else {
button.click({ clone: this }, function(e) {
e.preventDefault();
var clone = e.data.clone;
removeClone(clone);
} );
}
};
// this.addSubmitButton(button);
Clone.prototype.addSubmitButton = function(button) {
this.submitButtons.push(button);
button.click({ clone: this }, function(e) {
e.preventDefault();
var clone = e.data.clone;
console.log('clicked! ', clone );
// Add header and fold. No adding additional clone.
clone.master.addHeader( clone.fold().createHeader() );
clone.master.actions.show();
});
};
//
Clone.prototype.highlightHeader = function() {
if( !this.header ) { return false; }
this.header.removeClass('panel-default').addClass('panel-secondary');
return this;
};
Clone.prototype.edit = function() {
};
// this.createHeader(editLink);
Clone.prototype.createHeader = function(editLink) {
// editLink = (!!editLink) ? editLink : false;
// if not passed vs if false.
if( editLink === undefined ) {
editLink = (!!this.editLink) ? this.editLink : false;
} else {
this.editLink = editLink;
}
if( !!this.header ) {
return this.header;
}
var self = this;
var title = $('<h4>', { class: 'panel-title panel-'+ this.iterator }).prepend(
$.map( this.fieldset.find('[data-title-'+ self.iterator +']'), function(n) {
var value = ( !!$(n).data( $(n).data('title-'+self.iterator)) ) ? $(n).data( $(n).data('title-'+self.iterator) ) : $(n).val();
if( $(n).is(':radio') && !$(n).is(':checked') ) { return ''; }
return $('<span>', { class: [$(n).data('title-'+ self.iterator), 'title'].join(' ') }).text( value );
})
);
if( !!editLink ) {
var link = $('<a>', { class: 'edit-link' }).attr('href', '#').text('Edit').prepend($('<i>', { class: 'glyphicon glyphicon-pencil' }));
title.append( link );
var areClones;
link.on('click', function(e) {
e.preventDefault();
self.master.foldAll().clearSummary().allHeaders(false);
self.highlightHeader().unfold();
$(this).detach();
var driver = self.master,
summaryButton = $('<button>', { class: 'btn btn-lg btn-success pull-right col-xs-12 col-sm-6 col-lg-5' }).text('I\'ve finished ▶'),
nextButton = self.fieldset.closest('form').find('#form-actions .btn-success');
// $(document).trigger('cloneEdit', [self, driver]);
if( !!areClones ) {
return true;
} else {
summaryButton.on('click', function(e) {
e.preventDefault();
$(this).after(nextButton).detach();
driver.foldAll();
driver.clearSummary().allHeaders(true);
areClones = false;
// $(document).trigger('cloneSummary', [driver]);
});
// driver.clearSummary().allHeaders(false);
nextButton.after(summaryButton).detach();
areClones = true;
return true;
}
return true;
} );
}
var panelHeading = $('<div>', { class: 'panel-heading' }).append(title),
panel = $('<div>', { class: 'panel panel-default' });
var button, removeButton;
if( !!this.keepDefault ) {
if( !!this.isDefault ) {
button = $('<span>', { class: 'main-'+this.iterator }).attr('href','#').text('Main '+this.iterator.capitalize());
} else {
button = $('<a>', { class: 'make-main-'+this.iterator }).attr('href','#').text('Make main '+this.iterator);
}
this.addMakeDefaultButton(button);
}
if( !this.isDefault || !this.keepDefault ) {
removeButton = $('<a>', { class:'pull-right' }).attr('href','#').html( $('<i>', { class:'glyphicon glyphicon-trash' }) );
title.prepend( removeButton );
this.addRemoveButton(removeButton);
}
title.append( button );
this.header = panel.append( panelHeading );
return this.header;
};
// this.destroyHeader();
Clone.prototype.destroyHeader = function() {
if( !!this.header ) {
this.header.remove();
this.header = null;
}
return this;
};
//
// this.setID(n);
Clone.prototype.setID = function(n) {
var self = this, searchIterator = '{'+this.iterator+'}', _newName;
var replaceInstancesInAttr = function(attr, _this, self, searchIterator) {
// split string, change required element, join string, set name.
var _count, _newName, _matchInstances = function(name) {
var matches = name.substring(0, name.indexOf( searchIterator ) ).match(/_/g) || [];
return matches.length;
};
if( !!$(_this).data('iteration') ) {
var data = JSON.parse($(_this).data('iteration'));
if( data[self.iterator] ) {
_count = data[self.iterator];
} else {
_count = _matchInstances( $(_this).attr(attr) );
if( _count < 1 ) { return ''; }
data[self.iterator] = _count;
$(_this).data('iteration', JSON.stringify(data) );
}
} else {
_count = _matchInstances( $(_this).attr(attr) );
if( _count < 1 ) { return ''; }
var ob = { };
ob[self.iterator] = _count;
$(_this).data('iteration', JSON.stringify(ob) );
}
_newName = $(_this).attr(attr).split('_');
_newName[_count] = String(n);
return _newName.join('_');
};
this.fieldset.find(':input, [for]').each(function() {
switch(true) {
case !!$(this).attr('name'):
//
_newName = replaceInstancesInAttr('name', this, self, searchIterator);
if( !_newName ) { break; }
$(this).attr( 'name', _newName );
if( !!$(this).data('parsley-multiple') ) { $(this).attr('data-parsley-multiple', _newName); }
if( !!$(this).data('parsley-group') ) { $(this).attr('data-parsley-group', _newName); }
break;
// label
case !!$(this).attr('for'):
// Repeat for these tags.
_newName = replaceInstancesInAttr('for', this, self, searchIterator);
$(this).attr( 'for', _newName );
break;
// def case
default:
//
break;
// end switch.
}
});
// update IDs...
this.fieldset.attr('id', this.iterator+'-'+n );
this.id = n;
// Replace non-default questions required data.
// If the form group is not visible by default; fields should not be required.
// If these fields become visible, we need to enforce their requiredliness.
this.fieldset.find('.form-group.hidden').each( function(i,el) {
$(el).find('input[required]').each( function(i,el) {
$(el).attr('data-required', true).removeAttr('required');
});
});
// Check whether checkbox/radio is linked to further questions.
this.fieldset.find('input:checkbox, input:radio').on( 'change', function() {
// Match all inputs that are in groups/multiples and have data-checked attribute.
var _inputName = $(this).attr('name'),
_inputGroup = self.fieldset.find('input[name='+ _inputName +']'),
_hasSelect = $.inArray(true, _inputGroup.map( function() { return !!$(this).data('checked'); }) );
if( _hasSelect > -1 ) {
var fl, gl, _dataIdentifier,
_removeRequirement = function(i,el) { $(el).attr('required', true); },
_addRequirement = function(i,el) { $(el).removeAttr('required'); };
// Loop through each input within the group.
// Check whether there is show/hide data for each individual input.
// Show or hide based on whether the input has been selected/deselected.
// Edit: Loop and only hide. After loop perform events on selected. Allows reuse of ids.
for( gl=0 ; gl < _inputGroup.length ; gl++ ) {
_dataIdentifier = $(_inputGroup[gl]).data('checked') ? $(_inputGroup[gl]).data('checked').split(' ') : [] ;
if( _inputGroup[gl] !== $(this)[0] ) {
for( fl=0 ; fl < _dataIdentifier.length ; fl++ ) {
self.fieldset.find('[data-checked-id="'+_dataIdentifier[fl]+'"]').addClass('hidden');
$.each( self.fieldset.find('[data-checked-id="'+_dataIdentifier[fl]+'"]').find('[data-required]'), _addRequirement(fl,this) );
}
}
}
_dataIdentifier = $(this).data('checked') ? $(this).data('checked').split(' ') : [] ;
for( fl=0 ; fl < _dataIdentifier.length ; fl++ ) {
self.fieldset.find('[data-checked-id="'+_dataIdentifier[fl]+'"]').removeClass('hidden');
$.each( self.fieldset.find('[data-checked-id="'+_dataIdentifier[fl]+'"]').find('[data-required]'), _removeRequirement(fl,this) );
}
}
// If this item being selected also sets other input.
// EG Title:'Mr' autoselects option for Sex:'Male'
// get the set-if attribute and use to select input of that id.
// remove checked attribute from all, then target specific
var _setIf = '';
if( !!$(this).data('set-if') ) { _setIf = $(this).data('set-if').split(' '); }
if( !!_setIf.length ) {
var lf;
for( lf=0 ; lf < _setIf.length ; lf++ ) {
_inputName = self.fieldset.find('input#'+_setIf[lf]).attr('name');
self.fieldset.find('input[name='+ _inputName +']').removeAttr('checked');
// self.fieldset.find('input#'+_setIf[lf]).attr('checked', true).trigger('click');
}
}
});
//
return this;
};
// this.destruct();
Clone.prototype.destruct = function() {
this.fieldset.remove();
if( !!this.header ) {
this.destroyHeader();
}
this.master.setIDs();
return this;
};
// this.removeModal();
Clone.prototype.removeModal = function() {
// Create, add button, listen for click, detruct.
};
Clone.prototype.serialize = function() {
return this.fieldset.serialize();
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment