Source: jquery.add-input-area.js

/**
 * @file jQuery Plugin: jquery.add-input-area
 * @version 4.7.2
 * @author Yuusaku Miyazaki [toumin.m7@gmail.com]
 * @license MIT License
 */
(function($) {

/**
 * @desc プラグインをjQueryのプロトタイプに追加する
 * @global
 * @memberof jQuery
 * @param {Object} [option] オプションを格納した連想配列
 * @param {string} [option.attr_name] - 増減する要素のid属性の命名規則
 * @param {string} [option.area_var] - 増減する要素に共通するCSSクラス名
 * @param {string} [option.area_del] - 削除ボタンとともに表示・非表示が切り替わる削除エリアに共通するCSSクラス名。
 * @param {string} [option.btn_del] - 削除ボタンに共通するCSSクラス名
 * @param {string} [option.btn_add] - 追加ボタンに共通するCSSクラス名
 * @param {Function} [option.after_add] - 追加後に実行する関数
 * @param {number} [option.maximum] - 最大増加数
 */
$.fn.addInputArea = function(option) {
	return this.each(function() {
		new AddInputArea(this, option);
	});
};

/**
 * @global
 * @constructor
 * @classdesc 要素ごとに適用される処理を集めたクラス
 * @param {Object} elem - プラグインを適用するHTML要素
 * @param {Object} option - オプションを格納した連想配列
 *
 * @prop {Object} elem - プラグインを適用するHTML要素
 * @prop {Object} option - オプションを格納した連想配列
 */
function AddInputArea(elem, option) {
	this.elem = elem;
	this.option = option;

	this._setOption();
	this._setDelBtnVisibility();
	this._ehAddBtn();
	this._ehDelBtn();
	this._setNameAttribute();
	this._saveOriginal();
}

$.extend(AddInputArea.prototype, /** @lends AddInputArea.prototype */ {
	/**
	 * @private
	 * @desc オプションの初期化
	 */
	_setOption: function() {
		var id = $(this.elem).attr('id');
		this.option =  $.extend({
			attr_name : (id) ? id  + '_%d'       : 'aia_%d',
			area_var  : (id) ? '.' + id + '_var' : '.aia_var',
			area_del  : '',
			btn_del   : (id) ? '.' + id + '_del' : '.aia_del',
			btn_add   : (id) ? '.' + id + '_add' : '.aia_add',
			after_add : null,
			maximum   : 0
		}, this.option);
		if (!this.option.area_del) this.option.area_del = this.option.btn_del;
	},

	/**
	 * @private
	 * @desc 削除ボタンの表示状態を決定する。<br>
	 * 増減する項目がひとつなら、削除ボタンは表示しない。
	 */
	_setDelBtnVisibility: function() {
		if ($(this.elem).find(this.option.area_var).length == 1) {
			$(this.elem).find(this.option.area_del).hide();
		}
	},

	/**
	 * @private
	 * @desc 追加ボタンのイベントハンドラ
	 */
	_ehAddBtn: function() {
		var self = this;
		$(document).on('click', this.option.btn_add, function(ev) {
			// 品目入力欄を追加
			var len_list = $(self.elem).find(self.option.area_var).length;
			var new_list = $(self.option.original).clone(true);

			$(new_list)
				.find('[name]').each(function(idx, obj) {
					// name, id属性を変更
					self._changeAttrAlongFormat(obj, len_list, 'name');
					self._changeAttrAlongFormat(obj, len_list, 'id');

					// val, textを空にする。
					if ($(obj).attr('empty_val') != 'false') {
						if (
							$(obj).attr('type') == 'checkbox' ||
							$(obj).attr('type') == 'radio'
						) {
							obj.checked = false;
						} else if (
							$(obj).attr('type') != 'submit' &&
							$(obj).attr('type') != 'reset'  &&
							$(obj).attr('type') != 'image'  &&
							$(obj).attr('type') != 'button'
						) {
							$(obj).val('');
						}
					}
				}).end()
				.find('[for]').each(function(idx, obj) {
					// for属性を変更
					self._changeAttrAlongFormat(obj, len_list, 'for');
				});

			$(self.elem).append(new_list);
			// 入力欄が2つ以上になるので、削除ボタンを表示する
			$(self.elem).find(self.option.area_del).show();

			// 追加上限
			if (
				self.option.maximum > 0 &&
				$(self.elem).find(self.option.area_var).length >= self.option.maximum
			) {
				$(self.option.btn_add).hide();
			}
			// 追加後の処理があれば実行する
			if (typeof self.option.after_add == 'function') self.option.after_add();
		});
	},

	/**
	 * @private
	 * @desc 削除ボタンのイベントハンドラ
	 */
	_ehDelBtn: function() {
		var self = this;
		$(self.elem).on('click', self.option.btn_del, function(ev) {
			ev.preventDefault();
			// 品目入力欄を削除
			var idx = $(self.elem).find(self.option.btn_del).index(ev.target);
			$(self.elem).find(self.option.area_var).eq(idx).remove();

			// 削除ボタンの表示状態を決定する
			self._setDelBtnVisibility();

			// 入力欄の番号を振り直す
			self._setNameAttribute();

			// 追加上限
			if (
				self.option.maximum > 0 &&
				$(self.elem).find(self.option.area_var).length < self.option.maximum
			) {
				$(self.option.btn_add).show();
			}
		});
	},

	/**
	 * @private
	 * @desc 増減項目のid,name,for属性の番号を一括して振り直す
	 */
	_setNameAttribute: function() {
		var self = this;
		$(this.elem).find(this.option.area_var).each(function(idx, obj) {
			$(obj)
				.find('[name]').each(function() {
					// name, id属性を変更
					self._changeAttrAlongFormat(this, idx, 'name');
					self._changeAttrAlongFormat(this, idx, 'id');
				}).end()
				.find('[for]').each(function() {
					// for属性を変更
					self._changeAttrAlongFormat(this, idx, 'for');
				});
		});
	},

	/**
	 * @private
	 * @desc クローン元を保管する
	 */
	_saveOriginal: function() {
		this.option.original = $(this.elem).find(this.option.area_var).eq(0).clone(true);
	},

	/**
	 * @private
	 * @desc 入力欄の番号を振り直す
	 * @param {Object} obj - 番号を変更すべき項目を持つHTML要素
	 * @param {number} idx - 変更する値
	 * @param {string} type - 属性の名前 (e.g.: id, name, for)
	 */
	_changeAttrAlongFormat: function(obj, idx, type) {
		var changed = null;
		if (/^.+_\d+$/.test($(obj).attr(type))) {
			changed =  $(obj).attr(type).replace(/^(.+_)\d+$/, '$1' + idx);
		} else {
			// 命名規則に従っていないのに"name_format"や"id_format"を設定していないと例外を投げる
			try {
				switch (type) {
					case 'name':
						if ($(obj).attr('name_format')) {
							changed = $(obj).attr('name_format').replace('%d', idx);
						} else {
							throw new Error(
								'(jquery.addInputArea)\n' +
								'Not found "name_format" attribute in\n' +
								'<' + $(obj)[0].tagName + ' ' + type + '="' + $(obj).attr(type) + '">'
							);
						}
						break;

					case 'id':
						if ($(obj).attr('id_format')) {
							changed = $(obj).attr('id_format').replace('%d', idx);
						} else if ($(obj).attr('id')) { // そもそもid属性が存在しない場合を除く
							throw new Error(
								'(jquery.addInputArea)\n' +
								'Not found "name_format" attribute in\n' +
								'<' + $(obj)[0].tagName + ' ' + type + '="' + $(obj).attr(type) + '">'
							);
						}
						break;

					case 'for':
						if ($(obj).attr('id_format')) {
							changed = $(obj).attr('id_format').replace('%d', idx);
						} else {
							throw new Error(
								'(jquery.addInputArea)\n' +
								'Not found "name_format" attribute in\n' +
								'<' + $(obj)[0].tagName + ' ' + type + '="' + $(obj).attr(type) + '">'
							);
						}
						break;
				}
			} catch(e) {
				alert(e);
			}
		}
		$(obj).attr(type, changed);
	}
}); // end of "$.extend"

})( /** namespace */ jQuery);