// jquery.repeater version 1.2.1 // https://github.com/DubFriend/jquery.repeater // (MIT) 09-10-2016 // Brian Detering (http://www.briandetering.net/) (function ($) { 'use strict'; var identity = function (x) { return x; }; var isArray = function (value) { return $.isArray(value); }; var isObject = function (value) { return !isArray(value) && (value instanceof Object); }; var isNumber = function (value) { return value instanceof Number; }; var isFunction = function (value) { return value instanceof Function; }; var indexOf = function (object, value) { return $.inArray(value, object); }; var inArray = function (array, value) { return indexOf(array, value) !== -1; }; var foreach = function (collection, callback) { for(var i in collection) { if(collection.hasOwnProperty(i)) { callback(collection[i], i, collection); } } }; var last = function (array) { return array[array.length - 1]; }; var argumentsToArray = function (args) { return Array.prototype.slice.call(args); }; var extend = function () { var extended = {}; foreach(argumentsToArray(arguments), function (o) { foreach(o, function (val, key) { extended[key] = val; }); }); return extended; }; var mapToArray = function (collection, callback) { var mapped = []; foreach(collection, function (value, key, coll) { mapped.push(callback(value, key, coll)); }); return mapped; }; var mapToObject = function (collection, callback, keyCallback) { var mapped = {}; foreach(collection, function (value, key, coll) { key = keyCallback ? keyCallback(key, value) : key; mapped[key] = callback(value, key, coll); }); return mapped; }; var map = function (collection, callback, keyCallback) { return isArray(collection) ? mapToArray(collection, callback) : mapToObject(collection, callback, keyCallback); }; var pluck = function (arrayOfObjects, key) { return map(arrayOfObjects, function (val) { return val[key]; }); }; var filter = function (collection, callback) { var filtered; if(isArray(collection)) { filtered = []; foreach(collection, function (val, key, coll) { if(callback(val, key, coll)) { filtered.push(val); } }); } else { filtered = {}; foreach(collection, function (val, key, coll) { if(callback(val, key, coll)) { filtered[key] = val; } }); } return filtered; }; var call = function (collection, functionName, args) { return map(collection, function (object, name) { return object[functionName].apply(object, args || []); }); }; //execute callback immediately and at most one time on the minimumInterval, //ignore block attempts var throttle = function (minimumInterval, callback) { var timeout = null; return function () { var that = this, args = arguments; if(timeout === null) { timeout = setTimeout(function () { timeout = null; }, minimumInterval); callback.apply(that, args); } }; }; var mixinPubSub = function (object) { object = object || {}; var topics = {}; object.publish = function (topic, data) { foreach(topics[topic], function (callback) { callback(data); }); }; object.subscribe = function (topic, callback) { topics[topic] = topics[topic] || []; topics[topic].push(callback); }; object.unsubscribe = function (callback) { foreach(topics, function (subscribers) { var index = indexOf(subscribers, callback); if(index !== -1) { subscribers.splice(index, 1); } }); }; return object; }; // jquery.input version 0.0.0 // https://github.com/DubFriend/jquery.input // (MIT) 09-04-2014 // Brian Detering (http://www.briandetering.net/) (function ($) { 'use strict'; var createBaseInput = function (fig, my) { var self = mixinPubSub(), $self = fig.$; self.getType = function () { throw 'implement me (return type. "text", "radio", etc.)'; }; self.$ = function (selector) { return selector ? $self.find(selector) : $self; }; self.disable = function () { self.$().prop('disabled', true); self.publish('isEnabled', false); }; self.enable = function () { self.$().prop('disabled', false); self.publish('isEnabled', true); }; my.equalTo = function (a, b) { return a === b; }; my.publishChange = (function () { var oldValue; return function (e, domElement) { var newValue = self.get(); if(!my.equalTo(newValue, oldValue)) { self.publish('change', { e: e, domElement: domElement }); } oldValue = newValue; }; }()); return self; }; var createInput = function (fig, my) { var self = createBaseInput(fig, my); self.get = function () { return self.$().val(); }; self.set = function (newValue) { self.$().val(newValue); }; self.clear = function () { self.set(''); }; my.buildSetter = function (callback) { return function (newValue) { callback.call(self, newValue); }; }; return self; }; var inputEqualToArray = function (a, b) { a = isArray(a) ? a : [a]; b = isArray(b) ? b : [b]; var isEqual = true; if(a.length !== b.length) { isEqual = false; } else { foreach(a, function (value) { if(!inArray(b, value)) { isEqual = false; } }); } return isEqual; }; var createInputButton = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'button'; }; self.$().on('change', function (e) { my.publishChange(e, this); }); return self; }; var createInputCheckbox = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'checkbox'; }; self.get = function () { var values = []; self.$().filter(':checked').each(function () { values.push($(this).val()); }); return values; }; self.set = function (newValues) { newValues = isArray(newValues) ? newValues : [newValues]; self.$().each(function () { $(this).prop('checked', false); }); foreach(newValues, function (value) { self.$().filter('[value="' + value + '"]') .prop('checked', true); }); }; my.equalTo = inputEqualToArray; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputEmail = function (fig) { var my = {}, self = createInputText(fig, my); self.getType = function () { return 'email'; }; return self; }; var createInputFile = function (fig) { var my = {}, self = createBaseInput(fig, my); self.getType = function () { return 'file'; }; self.get = function () { return last(self.$().val().split('\\')); }; self.clear = function () { // http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery this.$().each(function () { $(this).wrap('
').closest('form').get(0).reset(); $(this).unwrap(); }); }; self.$().change(function (e) { my.publishChange(e, this); // self.publish('change', self); }); return self; }; var createInputHidden = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'hidden'; }; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputMultipleFile = function (fig) { var my = {}, self = createBaseInput(fig, my); self.getType = function () { return 'file[multiple]'; }; self.get = function () { // http://stackoverflow.com/questions/14035530/how-to-get-value-of-html-5-multiple-file-upload-variable-using-jquery var fileListObject = self.$().get(0).files || [], names = [], i; for(i = 0; i < (fileListObject.length || 0); i += 1) { names.push(fileListObject[i].name); } return names; }; self.clear = function () { // http://stackoverflow.com/questions/1043957/clearing-input-type-file-using-jquery this.$().each(function () { $(this).wrap('').closest('form').get(0).reset(); $(this).unwrap(); }); }; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputMultipleSelect = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'select[multiple]'; }; self.get = function () { return self.$().val() || []; }; self.set = function (newValues) { self.$().val( newValues === '' ? [] : isArray(newValues) ? newValues : [newValues] ); }; my.equalTo = inputEqualToArray; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputPassword = function (fig) { var my = {}, self = createInputText(fig, my); self.getType = function () { return 'password'; }; return self; }; var createInputRadio = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'radio'; }; self.get = function () { return self.$().filter(':checked').val() || null; }; self.set = function (newValue) { if(!newValue) { self.$().each(function () { $(this).prop('checked', false); }); } else { self.$().filter('[value="' + newValue + '"]').prop('checked', true); } }; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputRange = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'range'; }; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputSelect = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'select'; }; self.$().change(function (e) { my.publishChange(e, this); }); return self; }; var createInputText = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'text'; }; self.$().on('change keyup keydown', function (e) { my.publishChange(e, this); }); return self; }; var createInputTextarea = function (fig) { var my = {}, self = createInput(fig, my); self.getType = function () { return 'textarea'; }; self.$().on('change keyup keydown', function (e) { my.publishChange(e, this); }); return self; }; var createInputURL = function (fig) { var my = {}, self = createInputText(fig, my); self.getType = function () { return 'url'; }; return self; }; var buildFormInputs = function (fig) { var inputs = {}, $self = fig.$; var constructor = fig.constructorOverride || { button: createInputButton, text: createInputText, url: createInputURL, email: createInputEmail, password: createInputPassword, range: createInputRange, textarea: createInputTextarea, select: createInputSelect, 'select[multiple]': createInputMultipleSelect, radio: createInputRadio, checkbox: createInputCheckbox, file: createInputFile, 'file[multiple]': createInputMultipleFile, hidden: createInputHidden }; var addInputsBasic = function (type, selector) { var $input = isObject(selector) ? selector : $self.find(selector); $input.each(function () { var name = $(this).attr('name'); inputs[name] = constructor[type]({ $: $(this) }); }); }; var addInputsGroup = function (type, selector) { var names = [], $input = isObject(selector) ? selector : $self.find(selector); if(isObject(selector)) { inputs[$input.attr('name')] = constructor[type]({ $: $input }); } else { // group by name attribute $input.each(function () { if(indexOf(names, $(this).attr('name')) === -1) { names.push($(this).attr('name')); } }); foreach(names, function (name) { inputs[name] = constructor[type]({ $: $self.find('input[name="' + name + '"]') }); }); } }; if($self.is('input, select, textarea')) { if($self.is('input[type="button"], button, input[type="submit"]')) { addInputsBasic('button', $self); } else if($self.is('textarea')) { addInputsBasic('textarea', $self); } else if( $self.is('input[type="text"]') || $self.is('input') && !$self.attr('type') ) { addInputsBasic('text', $self); } else if($self.is('input[type="password"]')) { addInputsBasic('password', $self); } else if($self.is('input[type="email"]')) { addInputsBasic('email', $self); } else if($self.is('input[type="url"]')) { addInputsBasic('url', $self); } else if($self.is('input[type="range"]')) { addInputsBasic('range', $self); } else if($self.is('select')) { if($self.is('[multiple]')) { addInputsBasic('select[multiple]', $self); } else { addInputsBasic('select', $self); } } else if($self.is('input[type="file"]')) { if($self.is('[multiple]')) { addInputsBasic('file[multiple]', $self); } else { addInputsBasic('file', $self); } } else if($self.is('input[type="hidden"]')) { addInputsBasic('hidden', $self); } else if($self.is('input[type="radio"]')) { addInputsGroup('radio', $self); } else if($self.is('input[type="checkbox"]')) { addInputsGroup('checkbox', $self); } else { //in all other cases default to a "text" input interface. addInputsBasic('text', $self); } } else { addInputsBasic('button', 'input[type="button"], button, input[type="submit"]'); addInputsBasic('text', 'input[type="text"]'); addInputsBasic('password', 'input[type="password"]'); addInputsBasic('email', 'input[type="email"]'); addInputsBasic('url', 'input[type="url"]'); addInputsBasic('range', 'input[type="range"]'); addInputsBasic('textarea', 'textarea'); addInputsBasic('select', 'select:not([multiple])'); addInputsBasic('select[multiple]', 'select[multiple]'); addInputsBasic('file', 'input[type="file"]:not([multiple])'); addInputsBasic('file[multiple]', 'input[type="file"][multiple]'); addInputsBasic('hidden', 'input[type="hidden"]'); addInputsGroup('radio', 'input[type="radio"]'); addInputsGroup('checkbox', 'input[type="checkbox"]'); } return inputs; }; $.fn.inputVal = function (newValue) { var $self = $(this); var inputs = buildFormInputs({ $: $self }); if($self.is('input, textarea, select')) { if(typeof newValue === 'undefined') { return inputs[$self.attr('name')].get(); } else { inputs[$self.attr('name')].set(newValue); return $self; } } else { if(typeof newValue === 'undefined') { return call(inputs, 'get'); } else { foreach(newValue, function (value, inputName) { inputs[inputName].set(value); }); return $self; } } }; $.fn.inputOnChange = function (callback) { var $self = $(this); var inputs = buildFormInputs({ $: $self }); foreach(inputs, function (input) { input.subscribe('change', function (data) { callback.call(data.domElement, data.e); }); }); return $self; }; $.fn.inputDisable = function () { var $self = $(this); call(buildFormInputs({ $: $self }), 'disable'); return $self; }; $.fn.inputEnable = function () { var $self = $(this); call(buildFormInputs({ $: $self }), 'enable'); return $self; }; $.fn.inputClear = function () { var $self = $(this); call(buildFormInputs({ $: $self }), 'clear'); return $self; }; }(jQuery)); $.fn.repeaterVal = function () { var parse = function (raw) { var parsed = []; foreach(raw, function (val, key) { var parsedKey = []; if(key !== "undefined") { parsedKey.push(key.match(/^[^\[]*/)[0]); parsedKey = parsedKey.concat(map( key.match(/\[[^\]]*\]/g), function (bracketed) { return bracketed.replace(/[\[\]]/g, ''); } )); parsed.push({ val: val, key: parsedKey }); } }); return parsed; }; var build = function (parsed) { if( parsed.length === 1 && (parsed[0].key.length === 0 || parsed[0].key.length === 1 && !parsed[0].key[0]) ) { return parsed[0].val; } foreach(parsed, function (p) { p.head = p.key.shift(); }); var grouped = (function () { var grouped = {}; foreach(parsed, function (p) { if(!grouped[p.head]) { grouped[p.head] = []; } grouped[p.head].push(p); }); return grouped; }()); var built; if(/^[0-9]+$/.test(parsed[0].head)) { built = []; foreach(grouped, function (group) { built.push(build(group)); }); } else { built = {}; foreach(grouped, function (group, key) { built[key] = build(group); }); } return built; }; return build(parse($(this).inputVal())); }; $.fn.repeater = function (fig) { fig = fig || {}; var setList; $(this).each(function () { var $self = $(this); var show = fig.show || function () { $(this).show(); }; var hide = fig.hide || function (removeElement) { removeElement(); }; var $list = $self.find('[data-repeater-list]').first(); var $filterNested = function ($items, repeaters) { return $items.filter(function () { return repeaters ? $(this).closest( pluck(repeaters, 'selector').join(',') ).length === 0 : true; }); }; var $items = function () { return $filterNested($list.find('[data-repeater-item]'), fig.repeaters); }; var $itemTemplate = $list.find('[data-repeater-item]') .first().clone().hide(); var $firstDeleteButton = $filterNested( $filterNested($(this).find('[data-repeater-item]'), fig.repeaters) .first().find('[data-repeater-delete]'), fig.repeaters ); if(fig.isFirstItemUndeletable && $firstDeleteButton) { $firstDeleteButton.remove(); } var getGroupName = function () { var groupName = $list.data('repeater-list'); return fig.$parent ? fig.$parent.data('item-name') + '[' + groupName + ']' : groupName; }; var initNested = function ($listItems) { if(fig.repeaters) { $listItems.each(function () { var $item = $(this); foreach(fig.repeaters, function (nestedFig) { $item.find(nestedFig.selector).repeater(extend( nestedFig, { $parent: $item } )); }); }); } }; var $foreachRepeaterInItem = function (repeaters, $item, cb) { if(repeaters) { foreach(repeaters, function (nestedFig) { cb.call($item.find(nestedFig.selector)[0], nestedFig); }); } }; var setIndexes = function ($items, groupName, repeaters) { $items.each(function (index) { var $item = $(this); $item.data('item-name', groupName + '[' + index + ']'); $filterNested($item.find('[name]'), repeaters) .each(function () { var $input = $(this); // match non empty brackets (ex: "[foo]") var matches = $input.attr('name').match(/\[[^\]]+\]/g); var name = matches ? // strip "[" and "]" characters last(matches).replace(/\[|\]/g, '') : $input.attr('name'); var newName = groupName + '[' + index + '][' + name + ']' + ($input.is(':checkbox') || $input.attr('multiple') ? '[]' : ''); $input.attr('name', newName); $foreachRepeaterInItem(repeaters, $item, function (nestedFig) { var $repeater = $(this); setIndexes( $filterNested($repeater.find('[data-repeater-item]'), nestedFig.repeaters || []), groupName + '[' + index + ']' + '[' + $repeater.find('[data-repeater-list]').first().data('repeater-list') + ']', nestedFig.repeaters ); }); }); }); $list.find('input[name][checked]') .removeAttr('checked') .prop('checked', true); }; setIndexes($items(), getGroupName(), fig.repeaters); initNested($items()); if(fig.initEmpty) { $items().remove(); } if(fig.ready) { fig.ready(function () { setIndexes($items(), getGroupName(), fig.repeaters); }); } var appendItem = (function () { var setItemsValues = function ($item, data, repeaters) { if(data || fig.defaultValues) { var inputNames = {}; $filterNested($item.find('[name]'), repeaters).each(function () { var key = $(this).attr('name').match(/\[([^\]]*)(\]|\]\[\])$/)[1]; inputNames[key] = $(this).attr('name'); }); $item.inputVal(map( filter(data || fig.defaultValues, function (val, name) { return inputNames[name]; }), identity, function (name) { return inputNames[name]; } )); } $foreachRepeaterInItem(repeaters, $item, function (nestedFig) { var $repeater = $(this); $filterNested( $repeater.find('[data-repeater-item]'), nestedFig.repeaters ) .each(function () { var fieldName = $repeater.find('[data-repeater-list]').data('repeater-list'); if(data && data[fieldName]) { var $template = $(this).clone(); $repeater.find('[data-repeater-item]').remove(); foreach(data[fieldName], function (data) { var $item = $template.clone(); setItemsValues( $item, data, nestedFig.repeaters || [] ); $repeater.find('[data-repeater-list]').append($item); }); } else { setItemsValues( $(this), nestedFig.defaultValues, nestedFig.repeaters || [] ); } }); }); }; return function ($item, data) { $list.append($item); setIndexes($items(), getGroupName(), fig.repeaters); $item.find('[name]').each(function () { $(this).inputClear(); }); setItemsValues($item, data || fig.defaultValues, fig.repeaters); }; }()); var addItem = function (data) { var $item = $itemTemplate.clone(); appendItem($item, data); if(fig.repeaters) { initNested($item); } show.call($item.get(0)); }; setList = function (rows) { $items().remove(); foreach(rows, addItem); }; $filterNested($self.find('[data-repeater-create]'), fig.repeaters).click(function () { addItem(); }); $list.on('click', '[data-repeater-delete]', function () { var self = $(this).closest('[data-repeater-item]').get(0); hide.call(self, function () { $(self).remove(); setIndexes($items(), getGroupName(), fig.repeaters); }); }); }); this.setList = setList; return this; }; }(jQuery));