commit 08ec2d996099baf305efd8f28ee267c7fd043390
Author: Stephen Beaver <sbeaver@netgate.com>
Date:   Fri Oct 2 14:57:24 2015 -0400

    Completes #5159
    All duplicated JS removed to included file

diff --git a/src/usr/local/www/firewall_aliases_edit.php b/src/usr/local/www/firewall_aliases_edit.php
index 5146fac01a..b2940a946f 100755
--- a/src/usr/local/www/firewall_aliases_edit.php
+++ b/src/usr/local/www/firewall_aliases_edit.php
@@ -733,155 +733,7 @@ print $form;
 <script>
 //<![CDATA[
 events.push(function(){
-
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-
-		$('[id^=address]').autocomplete({
-			source: addressarray
-		});
-	}
-
+	
 	function typechange() {
 		var tab = $('#type').find('option:selected').val();
 		$("[id^='address_subnet']").prop("disabled", (tab == 'host') || (tab == 'port') || (tab == 'url') || (tab == 'url_ports'));
@@ -899,10 +751,6 @@ events.push(function(){
 		$('.repeatable:first').find('label').text(labelstr[tab]);
 	}
 
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
 	// On load . .
 	typechange();
 
@@ -914,19 +762,6 @@ events.push(function(){
 	});
 
 	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-
 	$('#type').on('change', function() {
 		typechange();
 	});
diff --git a/src/usr/local/www/jquery/pfSenseHelpers.js b/src/usr/local/www/jquery/pfSenseHelpers.js
index c814b61a0b..8fb24fb239 100644
--- a/src/usr/local/www/jquery/pfSenseHelpers.js
+++ b/src/usr/local/www/jquery/pfSenseHelpers.js
@@ -1,3 +1,55 @@
+/* ====================================================================
+ *	Copyright (c)  2004-2015  Electric Sheep Fencing, LLC. All rights reserved.
+ *	Copyright (c)  2004, 2005 Scott Ullrich
+ *
+ *	Redistribution and use in source and binary forms, with or without modification,
+ *	are permitted provided that the following conditions are met:
+ *
+ *	1. Redistributions of source code must retain the above copyright notice,
+ *		this list of conditions and the following disclaimer.
+ *
+ *	2. Redistributions in binary form must reproduce the above copyright
+ *		notice, this list of conditions and the following disclaimer in
+ *		the documentation and/or other materials provided with the
+ *		distribution.
+ *
+ *	3. All advertising materials mentioning features or use of this software
+ *		must display the following acknowledgment:
+ *		"This product includes software developed by the pfSense Project
+ *		 for use in the pfSense software distribution. (http://www.pfsense.org/).
+ *
+ *	4. The names "pfSense" and "pfSense Project" must not be used to
+ *		 endorse or promote products derived from this software without
+ *		 prior written permission. For written permission, please contact
+ *		 coreteam@pfsense.org.
+ *
+ *	5. Products derived from this software may not be called "pfSense"
+ *		nor may "pfSense" appear in their names without prior written
+ *		permission of the Electric Sheep Fencing, LLC.
+ *
+ *	6. Redistributions of any form whatsoever must retain the following
+ *		acknowledgment:
+ *
+ *	"This product includes software developed by the pfSense Project
+ *	for use in the pfSense software distribution (http://www.pfsense.org/).
+ *
+ *	THIS SOFTWARE IS PROVIDED BY THE pfSense PROJECT ``AS IS'' AND ANY
+ *	EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ *	IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ *	PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE pfSense PROJECT OR
+ *	ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ *	NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ *	LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ *	HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ *	STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ *	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ *	OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ *	====================================================================
+ *
+ */
+
 // These helper functions are used on many/most UI pages to hide/show/disable/enable form elements where required
 
 // Hides the <div> in which the specified input element lives so that the input, its label and help text are hidden
@@ -85,4 +137,177 @@ function hideLabel(text, hide) {
 		element.parent('div').addClass('hidden');
 	else
 		element.parent('div').removeClass('hidden');
-}
\ No newline at end of file
+}
+
+// The followinf functions are used by form froups assigned a class of "repeatable" and provide the ability
+// to add/delete new rows of sequentially numbered elements, their labels and their help text
+// See firewall_aliases_edit.php for an example
+
+function setMasks() {
+	// Find all ipaddress masks and make dynamic based on address family of input
+	$('span.pfIpMask + select').each(function (idx, select){
+		var input = $(select).prevAll('input[type=text]');
+
+		input.on('change', function(e){
+			var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
+			if (!isV6)
+				max = 32;
+
+			if (input.val() == "")
+				return;
+
+			while (select.options.length > max)
+				select.remove(0);
+
+			if (select.options.length < max)
+			{
+				for (var i=select.options.length; i<=max; i++)
+					select.options.add(new Option(i, i), 0);
+			}
+		});
+
+		// Fire immediately
+		input.change();
+	});
+}
+
+// Complicated function to move all help text associated with this input id to the same id
+// on the row above. That way if you delete the last row, you don't lose the help
+function moveHelpText(id) {
+	$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
+		var fromId = this.id;
+		var toId = decrStringInt(fromId);
+		var helpSpan;
+
+		if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
+
+			helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
+			if($(helpSpan).hasClass('help-block')) {
+				if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
+					$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
+				else
+					$('#' + decrStringInt(fromId)).after(helpSpan);
+			}
+		}
+	});
+}
+
+// Increment the number at the end of the string
+function bumpStringInt( str )	{
+  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
+
+  if( data )
+	newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
+
+  return newStr || str;
+}
+
+// Decrement the number at the end of the string
+function decrStringInt( str )	{
+  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
+
+  if( data )
+	newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
+
+  return newStr || str;
+}
+
+// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
+// gaps, but I do :)
+function renumber() {
+	var idx = 0;
+
+	$('.repeatable').each(function() {
+
+		$(this).find('input').each(function() {
+			$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
+			$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
+		});
+
+		$(this).find('select').each(function() {
+			$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
+			$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
+		});
+
+		$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
+
+		idx++;
+	});
+}
+
+function delete_row(row) {
+	$('#' + row).parent('div').parent('div').remove();
+	renumber();
+}
+
+function add_row() {
+	// Find the lst repeatable group
+	var lastRepeatableGroup = $('.repeatable:last');
+
+	// Clone it
+	var newGroup = lastRepeatableGroup.clone(true);
+
+	// Increment the suffix number for each input elemnt in the new group
+	$(newGroup).find('input').each(function() {
+		$(this).prop("id", bumpStringInt(this.id));
+		$(this).prop("name", bumpStringInt(this.name));
+		if(!$(this).is('[id^=delete]'))
+			$(this).val('');
+	});
+
+	// Do the same for selectors
+	$(newGroup).find('select').each(function() {
+		$(this).prop("id", bumpStringInt(this.id));
+		$(this).prop("name", bumpStringInt(this.name));
+		// If this selector lists mask bits, we need it to be reset to all 128 options
+		// and no items selected, so that automatic v4/v6 selection still works
+		if($(this).is('[id^=address_subnet]')) {
+			$(this).empty();
+			for(idx=128; idx>0; idx--) {
+				$(this).append($('<option>', {
+					value: idx,
+					text: idx
+				}));
+			}
+		}
+	});
+
+	// And for "for" tags
+	$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
+	$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
+
+	// Insert the updated/cloned row
+	$(lastRepeatableGroup).after(newGroup);
+
+	// Delete any help text from the group we have cloned
+	$(lastRepeatableGroup).find('.help-block').each(function() {
+		if((typeof retainhelp) == "undefined")
+			$(this).remove();
+	});
+
+	setMasks();
+
+	$('[id^=address]').autocomplete({
+		source: addressarray
+	});
+}
+
+// These are action buttons, not submit buttons
+$('[id^=addrow]').prop('type','button');
+$('[id^=delete]').prop('type','button');
+
+// on click . .
+$('[id^=addrow]').click(function() {
+	add_row();
+});
+
+$('[id^=delete]').click(function(event) {
+	if($('.repeatable').length > 1) {
+		if((typeof retainhelp) == "undefined")
+			moveHelpText(event.target.id);
+
+		delete_row(event.target.id);
+	}
+	else
+		alert('You may not delete the last row!');
+});
\ No newline at end of file
diff --git a/src/usr/local/www/pkg_edit.php b/src/usr/local/www/pkg_edit.php
index a18303db75..3de6048318 100644
--- a/src/usr/local/www/pkg_edit.php
+++ b/src/usr/local/www/pkg_edit.php
@@ -1412,172 +1412,6 @@ if ($pkg['fields']['field'] != "") { ?>
 //<![CDATA[
 	events.push(function(){
 
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-
-		$('[id^=address]').autocomplete({
-			source: addressarray
-		});
-	}
-
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-
 	// Hide the advanced section
 	var advanced_visible = false;
 
diff --git a/src/usr/local/www/services_dhcp.php b/src/usr/local/www/services_dhcp.php
index 52969cbe4b..43a17c1b65 100644
--- a/src/usr/local/www/services_dhcp.php
+++ b/src/usr/local/www/services_dhcp.php
@@ -1338,171 +1338,6 @@ if (!is_numeric($pool) && !($act == "newpool")) {
 //<![CDATA[
 events.push(function(){
 
-	// Row add/delete function for class="repeatable" =================================================================
-
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-
-	}
-
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-
 	// Show advanced DNS options ======================================================================================
 	var showadvdns = false;
 
diff --git a/src/usr/local/www/services_ntpd.php b/src/usr/local/www/services_ntpd.php
index 7422155462..36af8b7585 100644
--- a/src/usr/local/www/services_ntpd.php
+++ b/src/usr/local/www/services_ntpd.php
@@ -501,176 +501,16 @@ print($form);
 
 ?>
 
+<script>
+	// If this variable is declared, any help text will not be deleted when rows are added
+	// IOW the help text will appear on every row
+	retainhelp = true;
+</script>
+
 <script>
 //<![CDATA[
 events.push(function(){
 
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-/*
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-*/
-		setMasks();
-
-		$('[id^=address]').autocomplete({
-			source: addressarray
-		});
-	}
-
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-	//		moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-
 	// Make the ‘clear’ button a plain button, not a submit button
 	$('#btnadvstats').prop('type','button');
 
diff --git a/src/usr/local/www/services_router_advertisements.php b/src/usr/local/www/services_router_advertisements.php
index 6dc2babc27..8c001f924d 100644
--- a/src/usr/local/www/services_router_advertisements.php
+++ b/src/usr/local/www/services_router_advertisements.php
@@ -372,7 +372,7 @@ $form->add($section);
 
 $section = new Form_Section('DNS Configuration');
 
-for($idx=1; $idx=<4; $idx++) {
+for($idx=1; $idx<=4; $idx++) {
 	$section->addInput(new Form_IpAddress(
 		'radns' . $idx,
 		'Server ' . $idx,
@@ -410,168 +410,6 @@ print($form);
 //<![CDATA[
 events.push(function(){
 
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-	}
-
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-
 	// --------- Autocomplete -----------------------------------------------------------------------------------------
 	var addressarray = <?= json_encode(get_alias_list(array("host", "network", "openvpn", "urltable"))) ?>;
 
diff --git a/src/usr/local/www/services_unbound_acls.php b/src/usr/local/www/services_unbound_acls.php
index 5de889a673..a9d10768c2 100644
--- a/src/usr/local/www/services_unbound_acls.php
+++ b/src/usr/local/www/services_unbound_acls.php
@@ -269,7 +269,7 @@ if($act=="new" || $act=="edit") {
 		$counter++;
 	}
 
-	$section->addInput(new Form_Button(
+	$form->addGlobal(new Form_Button(
 		'addrow',
 		'Add network'
 	))->removeClass('btn-primary')->addClass('btn-success');
@@ -328,177 +328,4 @@ else // NOT 'edit' or 'add'
 <?php
 }
 
-?>
-<script>
-//<![CDATA[
-// Most of this needs to live in a common include file. It will be moved before production release.
-events.push(function(){
-
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=mask]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-	}
-
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-});
-//]]>
-</script>
-
-<?php
-include("foot.inc");
\ No newline at end of file
+include("foot.inc");
diff --git a/src/usr/local/www/system_certmanager.php b/src/usr/local/www/system_certmanager.php
index 3e8cb26008..8d0fd65d49 100644
--- a/src/usr/local/www/system_certmanager.php
+++ b/src/usr/local/www/system_certmanager.php
@@ -1058,168 +1058,6 @@ print $form;
 //<![CDATA[
 events.push(function(){
 
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-	}
-
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// on click . .
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
-
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
-
 <?php if ($internal_ca_count): ?>
 	function internalca_change() {
 
diff --git a/src/usr/local/www/vpn_pppoe_edit.php b/src/usr/local/www/vpn_pppoe_edit.php
index 6bc26740c5..d6540a5003 100644
--- a/src/usr/local/www/vpn_pppoe_edit.php
+++ b/src/usr/local/www/vpn_pppoe_edit.php
@@ -543,10 +543,17 @@ if ($usernames != ""){
 	}
 }
 
-$section->addInput(new Form_Button(
+$btnaddrow = new Form_Button(
 	'addrow',
-	'Add'
-))->removeClass('btn-primary')->addClass('btn-success');
+	'Add user'
+);
+
+$btnaddrow->removeClass('btn-primary')->addClass('btn-success');
+
+$section->addInput(new Form_StaticText(
+	null,
+	'&nbsp;' . $btnaddrow
+));
 
 // Hidden fields
 if(isset($id)) {
@@ -576,155 +583,6 @@ print_info_box(gettext('Don\'t forget to add a firewall rule to permit traffic f
 <script>
 //<![CDATA[
 events.push(function(){
-	function setMasks() {
-		// Find all ipaddress masks and make dynamic based on address family of input
-		$('span.pfIpMask + select').each(function (idx, select){
-			var input = $(select).prevAll('input[type=text]');
-
-			input.on('change', function(e){
-				var isV6 = (input.val().indexOf(':') != -1), min = 0, max = 128;
-				if (!isV6)
-					max = 32;
-
-				if (input.val() == "")
-					return;
-
-				while (select.options.length > max)
-					select.remove(0);
-
-				if (select.options.length < max)
-				{
-					for (var i=select.options.length; i<=max; i++)
-						select.options.add(new Option(i, i), 0);
-				}
-			});
-
-			// Fire immediately
-			input.change();
-		});
-	}
-
-	// Complicated function to move all help text associated with this input id to the same id
-	// on the row above. That way if you delete the last row, you don't lose the help
-	function moveHelpText(id) {
-		$('#' + id).parent('div').parent('div').find('input').each(function() {	 // For each <span></span>
-			var fromId = this.id;
-			var toId = decrStringInt(fromId);
-			var helpSpan;
-
-			if(!$(this).hasClass('pfIpMask') && !$(this).hasClass('btn')) {
-
-				helpSpan = $('#' + fromId).parent('div').parent('div').find('span:last').clone();
-				if($(helpSpan).hasClass('help-block')) {
-					if($('#' + decrStringInt(fromId)).parent('div').hasClass('input-group'))
-						$('#' + decrStringInt(fromId)).parent('div').after(helpSpan);
-					else
-						$('#' + decrStringInt(fromId)).after(helpSpan);
-				}
-			}
-		});
-	}
-
-	// Increment the number at the end of the string
-	function bumpStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) + 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Decrement the number at the end of the string
-	function decrStringInt( str )	{
-	  var data = str.match(/(\D*)(\d+)(\D*)/), newStr = "";
-
-	  if( data )
-		newStr = data[ 1 ] + ( Number( data[ 2 ] ) - 1 ) + data[ 3 ];
-
-	  return newStr || str;
-	}
-
-	// Called after a delete so that there are no gaps in the numbering. Most of the time the config system doesn't care about
-	// gaps, but I do :)
-	function renumber() {
-		var idx = 0;
-
-		$('.repeatable').each(function() {
-
-			$(this).find('input').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('select').each(function() {
-				$(this).prop("id", this.id.replace(/\d+$/, "") + idx);
-				$(this).prop("name", this.name.replace(/\d+$/, "") + idx);
-			});
-
-			$(this).find('label').attr('for', $(this).find('label').attr('for').replace(/\d+$/, "") + idx);
-
-			idx++;
-		});
-	}
-
-	function delete_row(row) {
-		$('#' + row).parent('div').parent('div').remove();
-		renumber();
-	}
-
-	function add_row() {
-		// Find the lst repeatable group
-		var lastRepeatableGroup = $('.repeatable:last');
-
-		// Clone it
-		var newGroup = lastRepeatableGroup.clone(true);
-
-		// Increment the suffix number for each input elemnt in the new group
-		$(newGroup).find('input').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			if(!$(this).is('[id^=delete]'))
-				$(this).val('');
-		});
-
-		// Do the same for selectors
-		$(newGroup).find('select').each(function() {
-			$(this).prop("id", bumpStringInt(this.id));
-			$(this).prop("name", bumpStringInt(this.name));
-			// If this selector lists mask bits, we need it to be reset to all 128 options
-			// and no items selected, so that automatic v4/v6 selection still works
-			if($(this).is('[id^=address_subnet]')) {
-				$(this).empty();
-				for(idx=128; idx>0; idx--) {
-					$(this).append($('<option>', {
-						value: idx,
-						text: idx
-					}));
-				}
-			}
-		});
-
-		// And for "for" tags
-		$(newGroup).find('label').attr('for', bumpStringInt($(newGroup).find('label').attr('for')));
-		$(newGroup).find('label').text(""); // Clear the label. We only want it on the very first row
-
-		// Insert the updated/cloned row
-		$(lastRepeatableGroup).after(newGroup);
-
-		// Delete any help text from the group we have cloned
-		$(lastRepeatableGroup).find('.help-block').each(function() {
-			$(this).remove();
-		});
-
-		setMasks();
-
-	}
-
-	// Disables the specified input element
-	function disableInput(id, disable) {
-		$('#' + id).prop("disabled", disable);
-	}
 
 	// show/hide radius server controls
 	function hide_radius(hide) {
@@ -764,27 +622,9 @@ events.push(function(){
 	});
 
 	// ---------- On initial page load ------------------------------------------------------------
-
 	hide_radius2(!$('#radiussecenable').prop('checked'));
 	hide_radius(!$('#radiusenable').prop('checked'));
-	// These are action buttons, not submit buttons
-	$('[id^=addrow]').prop('type','button');
-	$('[id^=delete]').prop('type','button');
-
-	// ---------- Click checkbox handlers ---------------------------------------------------------
-	
-	$('[id^=addrow]').click(function() {
-		add_row();
-	});
 
-	$('[id^=delete]').click(function(event) {
-		if($('.repeatable').length > 1) {
-			moveHelpText(event.target.id);
-			delete_row(event.target.id);
-		}
-		else
-			alert('<?php echo gettext("You may not delete the last one!")?>');
-	});
 });
 //]]>
 </script>
