Skip to content

Instantly share code, notes, and snippets.

@klaftertief
Created March 7, 2011 12:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save klaftertief/858462 to your computer and use it in GitHub Desktop.
Save klaftertief/858462 to your computer and use it in GitHub Desktop.
reducing duplicator
/**
* @package assets
*/
(function($) {
/**
* This plugin creates a Symphony duplicator.
*
* @param {Object} custom_settings
* An object with custom duplicator settings
*/
$.fn.symphonyDuplicator = function(custom_settings) {
var objects = this,
settings = {
instances: '> li:not(.template)', // What children do we use as instances?
templates: '> li.template', // What children do we use as templates?
headers: '> :first-child', // What part of an instance is the header?
orderable: false, // Can instances be ordered?
collapsible: false, // Can instances be collapsed?
constructable: true, // Allow construction of new instances?
destructable: true, // Allow destruction of instances?
minimum: 0, // Do not allow instances to be removed below this limit.
maximum: 1000, // Do not allow instances to be added above this limit.
speed: 'fast', // Control the speed of any animations
delay_initialize: false
};
$.extend(settings, custom_settings);
/*-----------------------------------------------------------------------*/
// Language strings
Symphony.Language.add({
'Add item': false,
'Remove item': false,
'Expand all': false,
'Collapse all': false
});
// Collapsible
if(settings.collapsible) {
objects = objects.symphonyCollapsible({
items: '.instance',
handles: '.header span'
});
}
// Orderable
if(settings.orderable) {
objects = objects.symphonyOrderable({
items: '.instance',
handles: '.header'
});
}
// Duplicator
objects = objects.map(function() {
var object = this,
templates = [],
widgets = {
controls: null,
selector: null,
constructor: null,
topcontrols: null,
collapser: null
},
silence = function() {
return false;
};
// Construct a new instance
var construct = function(source) {
var template = $(source).clone(true),
instance = prepare(template);
widgets.controls.before(instance);
object.trigger('construct', [instance]);
refresh(true);
updateUniqness();
return instance;
};
var destruct = function(source) {
var instance = $(source).remove();
object.trigger('destruct', [instance]);
refresh();
updateUniqness();
return instance;
};
// Prepare an instance
var prepare = function(source) {
var instance = $(source).addClass('instance expanded'),
header = instance.find(settings.headers).addClass('header').wrapInner('<span />'),
destructor = header.append('<a class="destructor" />').find('a.destructor:first').text(Symphony.Language.get('Remove item'));
instance.attr('data-type', header.find('span').get(0).childNodes[0].nodeValue + header.find('span').children().first().text());
header.nextAll().wrapAll('<div class="content" />');
destructor.bind('click.duplicator', function() {
if($(this).hasClass('disabled')) {
return;
}
destruct(source);
});
header.bind('selectstart.duplicator', silence);
return instance;
};
// Refresh disabled states
var refresh = function(input_focus) {
var constructor = settings.constructable,
selector = settings.constructable,
destructor = settings.destructable,
instances = object.children('.instance'),
empty = false;
// Update field names
instances.each(function(position) {
$(this).find('*[name]').each(function() {
var exp = /\[\-?[0-9]+\]/,
name = $(this).attr('name');
if (exp.test(name)) {
$(this).attr('name', name.replace(exp, '[' + position + ']'));
}
});
});
// Give focus to the first input in the first instance
if(input_focus) {
instances.filter(':last').find('input[type!="hidden"]:first').focus();
}
// No templates to add
if(templates.length < 1) {
constructor = false;
}
// Only one template
if(templates.length <= 1) {
selector = false;
}
// Maximum reached?
if(settings.maximum <= instances.length) {
constructor = false;
selector = false;
}
// Minimum reached?
if(settings.minimum >= instances.length) {
destructor = false;
}
// Constructor?
if(constructor) {
widgets.constructor.removeClass('disabled');
}
else {
widgets.constructor.addClass('disabled');
}
// Selector?
if(selector) {
widgets.selector.removeClass('disabled');
}
else {
widgets.selector.addClass('disabled');
}
// Destructor?
if(destructor) {
instances.find(settings.headers).find('.destructor').removeClass('disabled');
}
else {
instances.find(settings.headers).find('.destructor').addClass('disabled');
}
// Empty?
if(!empty) {
object.removeClass('empty');
}
else {
object.addClass('empty');
}
// Collapsible?
if(settings.collapsible) {
object.collapsible.initialize();
}
// Orderable?
if(settings.orderable) {
object.orderable.initialize();
}
};
// Update uniqueness
var updateUniqness = function() {
var instances = object.children('.instance'),
options = widgets.selector.find('option');
options.attr('disabled', false);
instances.each(function(position) {
var instance = $(this);
if (instance.hasClass('unique')) {
options.filter('[data-type=' + instance.attr('data-type') + ']').attr('disabled', 'disabled');
if (options.not(':disabled').length === 0) {
widgets.selector.prepend('<option class="empty"/>');
widgets.constructor.addClass('disabled');
} else {
options.filter('.empty').remove();
};
widgets.selector.find('option').not(':disabled').first().attr('selected', 'selected');
};
});
};
var collapsingEnabled = function() {
widgets.topcontrols.removeClass('hidden');
widgets.collapser.removeClass('disabled');
};
var collapsingDisabled = function() {
widgets.topcontrols.addClass('hidden');
widgets.collapser.addClass('disabled');
};
var toCollapseAll = function() {
widgets.collapser.removeClass('compact').text(Symphony.Language.get('Collapse all'));
};
var toExpandAll = function() {
widgets.collapser.addClass('compact').text(Symphony.Language.get('Expand all'));
};
/*-------------------------------------------------------------------*/
if (object instanceof $ === false) {
object = $(object);
}
object.duplicator = {
refresh: function() {
refresh();
},
initialize: function() {
object.addClass('duplicator');
// Prevent collapsing when ordering stops:
object.bind('orderstart.duplicator', function() {
if (settings.collapsible) {
object.collapsible.cancel();
}
});
// Refresh on reorder:
object.bind('orderstop.duplicator', function() {
refresh();
});
// Slide up on collapse:
object.bind('collapsestop.duplicator', function(event, item) {
item.find('> .content').show().slideUp(settings.speed);
});
// Slide down on expand:
object.bind('expandstop.duplicator', function(event, item) {
item.find('> .content').hide().slideDown(settings.speed);
});
widgets.controls = object.append('<div class="controls" />').find('> .controls:last');
widgets.selector = widgets.controls.prepend('<select />').find('> select:first');
widgets.constructor = widgets.controls.append('<a class="constructor" />').find('> a.constructor:first').text(Symphony.Language.get('Add item'));
// Prepare instances:
object.find(settings.instances).each(function() {
var instance = prepare(this);
object.trigger('construct', [instance]);
});
// Store templates:
object.find(settings.templates).each(function(position) {
var template = $(this).clone(true),
header = template.find(settings.headers).addClass('header'),
option = widgets.selector.append('<option />').find('option:last'),
header_children = header.children();
if(header_children.length) {
header_text = header.get(0).childNodes[0].nodeValue + ' (' + header_children.filter(':eq(0)').text() + ')';
}
else {
header_text = header.text();
}
option.text(header_text).val(position).attr('data-type', header.get(0).childNodes[0].nodeValue + header_children.filter(':eq(0)').text());
template.attr('data-type', header.get(0).childNodes[0].nodeValue + header_children.filter(':eq(0)').text());
// console.log(template);
if (template.hasClass('unique')) {
option.attr('data-unique', 'true');
};
// HACK: preselect Text Input for Section editor
if (header_text == 'Text Input') {
option.attr('selected', 'selected');
}
templates.push(template.removeClass('template'));
// Remove template source
$(this).remove();
});
// Construct new template:
widgets.constructor.bind('selectstart.duplicator', silence);
widgets.constructor.bind('mousedown.duplicator', silence);
widgets.constructor.bind('click.duplicator', function() {
if($(this).hasClass('disabled')) {
return;
}
var position = widgets.selector.val();
if(position >= 0) {
construct(templates[position]);
}
});
if(settings.collapsible) {
widgets.topcontrols = object
.prepend('<div class="controls top hidden" />')
.find('> .controls:first')
.append(widgets.controls
.prepend('<a class="collapser disabled" />')
.find('> a.collapser:first')
.text(Symphony.Language.get('Collapse all'))
.clone()
);
widgets.collapser = object.find('.controls > .collapser');
if(object.children('.instance').length > 0) {
collapsingEnabled();
}
object.bind('construct.duplicator', function() {
var instances = object.children('.instance');
if(instances.length > 0) {
collapsingEnabled();
}
});
object.bind('destruct.duplicator', function() {
var instances = object.children('.instance');
if(instances.length < 1) {
collapsingDisabled();
toCollapseAll();
}
});
object.bind('collapsestop.duplicator destruct.duplicator', function() {
if(object.has('.expanded').length == 0) {
toExpandAll();
}
});
object.bind('expandstop.duplicator destruct.duplicator', function() {
if(object.has('.collapsed').length == 0) {
toCollapseAll();
}
});
widgets.collapser.bind('click.duplicator', function() {
var item = $(this);
if(item.is('.disabled')) return;
object.duplicator[item.is('.compact') ? 'expandAll' : 'collapseAll']();
});
}
refresh();
updateUniqness();
},
expandAll: function() {
object.collapsible.expandAll();
toCollapseAll();
},
collapseAll: function() {
object.collapsible.collapseAll();
toExpandAll();
}
};
if (settings.delay_initialize !== true) {
object.duplicator.initialize();
}
return object;
});
return objects;
};
/**
* This plugin creates a Symphony duplicator with name.
*
* @param {Object} custom_settings
* An object with custom duplicator settings
*/
$.fn.symphonyDuplicatorWithName = function(custom_settings) {
var objects = $(this).symphonyDuplicator($.extend(
custom_settings, {
delay_initialize: true
}
));
objects = objects.map(function() {
var object = this;
object.bind('construct.duplicator', function(event, instance) {
var input = instance.find('input:visible:first'),
header = instance.find('.header:first > span:first'),
fallback = header.text(),
refresh = function() {
var value = input.val();
header.text(value ? value : fallback);
};
input.bind('change.duplicator', refresh).bind('keyup', refresh);
refresh();
});
object.duplicator.initialize();
});
return objects;
};
})(jQuery.noConflict());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment