A Pen by JonnyNinetoes

Thumbnail
This awesome code was written by JonnyNineToes, you can see more from this user in the personal repository.
You can find the original code on Codepen.io
Copyright JonnyNineToes ©

Technologies

  • HTML
  • CSS
  • JavaScript
<!DOCTYPE html>
<html lang="en" >

<head>
  <meta charset="UTF-8">
  <title>A Pen by  JonnyNinetoes</title>
  
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">

  <link rel='stylesheet prefetch' href='https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.css'>
<link rel='stylesheet prefetch' href='https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.css'>
<link rel='stylesheet prefetch' href='https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.structure.css'>
<link rel='stylesheet prefetch' href='https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.theme.css'>

      <link rel="stylesheet" href="css/style.css">

  
</head>

<body>

  <main class="container">
	<header class="no-print">
		<h1>Proposal Generator</h1>
	</header>
	
	<form id="frmInput" class="no-print">
		<datalist id="lstDataTam">
  			<option value="Tom Jones" data-title="Tiger" data-phone="8881234567" data-email="tom.jones@email.com">
			<option value="George Jones" data-title="No-Show Jones" data-phone="8881234568" data-email="george.jones@email.com">
			<option value="Mike Jones" data-title="Who?" data-phone="8881234569" data-email="mike.jones@email.com">
  		</datalist> 
		
		<div class="row">
			<!-- cannot use "date" input, it doesn't play nicely with jquery UI's date picker for some reason / pattern="/(0[1-9]|1[012])\/(0[1-9]|[12][0-9]|3[01])\/(19|20)\d\d/" -->
			<label class="one-half column">Today's Date: <input type="text" id="txtTodaysDate" placeholder="mm/dd/yyyy" maxlength="10" required></label>
		</div>

		<fieldset>
			<legend>Data TAM Info</legend>
			
			<div class="row">
				<label class="one-half column">Name: <input type="text" placeholder="" id="txtDataTamName" list="lstDataTam" required></label>

				<label class="one-half column">Job Title: <input type="text" placeholder="" id="txtDataTamTitle" required></label>
			</div>
			
			<div class="row">
				<!-- pattern="[\(]\d{3}[\)] \d{3}[\-]\d{4}" -->
				<label class="one-half column">Phone Number: <input type="tel" id="txtDataTamPhone" placeholder="(999) 999-9999" maxlength="24" required></label>

				<!-- pattern="/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/" -->
				<label class="one-half column">Email: <input type="email" id="txtDataTamEmail" placeholder="handle@email.com" required></label>
			</div>
		</fieldset>
		
		<fieldset>
			<legend>Customer Info</legend>
			
			<div class="row">
				<label class="one-half column">Name: <input type="text" id="txtCustomerName" required></label>
				<label class="one-half column">Logo: <input type="file" id="txtCustomerLogo" title="This field is currently disabled." disabled></label>
			</div>
		</fieldset>
		
		<div class="row">
			<label class="one-half column">Service Term: 
				<select id="ddlServiceTerm" required>
					<option value="" selected="selected"> - Select - </option>
					<option value="12"> 12 </option>
					<option value="24"> 24 </option>
					<option value="36"> 36 </option>
				</select>
			</label>
		</div>
		
		<table id="tblPriceComparison">
			<caption>Pricing Comparisons</caption>
			
			<thead>
				<tr>
					<th>&nbsp;</th>
					
					<th>Location</th>
					
					<th>Product Description</th>
					
					<th>Bandwidth</th>
					
					<th>Current MRC</th>
					
					<th>MRC <span id="spnSelectedServiceTerm">???</span> Mos</th>
					
					<th>NRC</th>
					
					<th>&nbsp;</th>
				</tr>
			</thead>
			
			<tbody class="repeater">
				<tr class="repeatable">
					<td>1</td>
					
					<td><input data-structure="txtLocation" id="txtLocation1" name="txtLocation1" placeholder="Address" required type="text"></td>
					
					<td><input data-structure="txtDescription" id="txtDescription1" name="txtDescription1" required type="text"></td>
					
					<td><input data-structure="txtBandwidth" id="txtBandwidth1" name="txtBandwidth1" required type="text"></td>
					
					<td><input data-structure="txtCurrentMRC" id="txtCurrentMRC1" name="txtCurrentMRC1" placeholder="$" required type="number" step="0.01" min="0" value="0"></td>
					
					<td><input data-structure="txtMRCXMonths" id="txtMRCXMonths1" name="txtMRCXMonths1" placeholder="$" required type="number" step="0.01" min="0" value="0"></td>
					
					<td><input data-structure="txtNRC" id="txtNRC1" name="txtNRC1" placeholder="$" required type="number" step="0.01" min="0" value="0"></td>
					
					<td>&nbsp;</td>
				</tr>
			</tbody>
			
			<tfoot>
				<tr>
					<td colspan="8"><input id="txtCount" type="hidden" value="1"></td>
				</tr>
			</tfoot>
		</table>

		<input type="submit" value="Generate" class="button-primary">
		<input type="reset" value="Reset">
	</form>

	<article id="artOutput"></article>
</main>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js'></script>

  

    <script  src="js/index.js"></script>




</body>

</html>

/*Downloaded from https://www.codeseek.co/JonnyNineToes/a-pen-by-jonnyninetoes-yKPeKy */
form {
	max-width: 1024px;
	margin: 0 auto;
}

fieldset {
	border: 1px solid rgb(209, 209, 209);
	border-radius: 0.5rem;
	padding-right: 2rem;
	padding-left: 2rem;
}

fieldset legend {
	padding: 0 0.5rem;
}

input[type="date"] {
	-webkit-appearance: none;
	-moz-appearance: none;
	appearance: none;
	height: 38px;
	padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
	background-color: #fff;
	border: 1px solid #D1D1D1;
	border-radius: 4px;
	box-shadow: none;
	box-sizing: border-box; 
}

input[type="number"] {
	text-align: right;
}

input[type="date"]:focus {
	border: 1px solid #33C3F0;
	outline: 0; 
}

input[type="text"], 
input[type="tel"], 
input[type="email"], 
input[type="date"], 
input[type="number"], 
select {
	width: 100%;
}

#tblPriceComparison {
	font-size: 75%;
}

#tblPriceComparison th, 
#tblPriceComparison td {
	padding: 4px 5px;
	vertical-align: middle;
}

#tblPriceComparison input {
	height: auto;
	margin-bottom: 0;
	padding: 3px 5px;
	line-height: normal;
}

tfoot td {
	text-align: center;
}

#spnSelectedServiceTerm {
	background-color: #FF0;
}

#artOutput {
	display: none;
	border: 1px solid #D1D1D1;
	padding: 1rem;
	border-radius: 0.5rem;
}

/* output styling */
#artOutput.container {
	position: relative;
	margin: 1in auto;
}

#artOutput #btnPrint {
	float: left;
	margin: 1rem;
}

#artOutput header img {
	display: block;	
	margin: 0 auto;
	width: auto;
	height: auto;
	max-width: 362px;
	max-height: 150px;
}

#artOutput header table {
	display: inline-block;
	border-collapse: collapse;
/* 	font-size: 75%; */
}

#artOutput header table caption {
	font-style: italic;
}

#artOutput header table th {
	text-align: right;
	font-style: italic;
}

#artOutput header table th, 
#artOutput header table td {
	padding: 3px;
	border-bottom: none;
} 

#tblFinalBreakdown {
	width: 100%;
	border-collapse: collapse;
	font-size: 80%;
}

#tblFinalBreakdown th {
	color: #FFF;
	background-color: #00F;
	border: 1px solid #00F;
}

#tblFinalBreakdown td {
	font-weight: normal;
	border: 1px solid #E1E1E1;
}

#tblFinalBreakdown th, 
#tblFinalBreakdown td {
	padding: 4px 15px;
} 

#tblFinalBreakdown td.invisible {
	border: none;
	text-align: right;
}

#tblFinalBreakdown td.currency:before {
    content: "$";
	position: absolute;
	left: 15px;
}

#tblFinalBreakdown td.currency {
	text-align: right;
	position: relative;
}

footer p {
	text-align: center;
}

.red {
	color: #F00;
	font-weight: bold;
	text-transform: uppercase;
	margin-bottom: 0;
}

.center {
	text-align: center;
}

/* print styles */
@media print {
	body {  
		font-size: 11pt; 
		width: 8.5in; 
	} 
	
	.no-print {  
		display: none; 
	} 
	
	#artOutput {
		border: none;
		padding: 0;
	}
}


/*Downloaded from https://www.codeseek.co/JonnyNineToes/a-pen-by-jonnyninetoes-yKPeKy */
$(document).ready( function () {
	// insert an "add more" button after the last element
	$('<input id="btnAdd" type="button" class="button-primary" value="Add Item">').insertAfter('#txtCount');
	
	// add the "remove" link to each section
	$('.repeatable').children('td').last().html('<a class="remove" title="Remove this item." href="javascript:void(0);">[x]</a>').text();
	
	updateRemoveLinks = function () {
		// if "repeatable" element count is greater than 1...
		if ($('.repeatable').length > 1) {
			 // ...show the "remove" link
			$('.repeatable').find('.remove').css({
				'display': 'inline'
			});
		} else {
			// ...don't display the "remove" link
			$('.repeatable').find('.remove').css({
				'display': 'none'
			});
		}
	}
	
	updateRemoveLinks();
	
	// does all the checking necessary for deleting an element
	destroy = function () {
		$('.remove').click( function () {
			// var result = confirm('Are you sure you want to delete row #' + $(this).parent('td').siblings().first().html());
			
			// when clicks the "[x]" link in a row, get link's "repeatable" parent (container) and remove it
			$(this).parent('td').parent('.repeatable').remove();

			// loop through all "repeatable" rows
			$('.repeatable').each( function () {
				var r = this;  // store "this", to avoid scope problems
				var num	= $(r).index() + 1; // store index+1 of current "repeatable" (zero-based)

				// re-number all rows
				$(r).children('td').first().html(num).text(); // value in first cell
				// loop through all input fields within
				$(r).find('input').each( function () {
					// get "structure" data attributes and add index+1 value 
					var dattr = $(this).data('structure') + num;
					
					// update "id" and "name" attributes
					$(this).attr({
						'id': dattr,
						'name': dattr
					});
				});
				
				// update "counter" field
				$('#txtCount').val( $('.repeatable').length );
			});

			updateRemoveLinks();
		});
	}

	// now, call the "destroy" method, so that when the page loads, this method is declared and will affect all "repeatable" elements - this has something to do with adding/removing dynamic elements, I wrote this so long ago, that I don't recall exactly how it works...
	destroy();
	
	// when the user clicks the "Add Item" button...
	$('#btnAdd').click( function () {
		// clone last row of table (a "repeatable" element), and then add clone to end of table (tbody section)
		$('.repeater').children('.repeatable').last().clone().appendTo('.repeater').html();

		// get the number of repeatable elements on the page
		var num = $('.repeatable').length;

		// re-number all rows
		$('.repeatable').last().children('td').first().html(num); // first cell of last row
		// loop through all input fields within last row
		$('.repeatable').last().find('input').each( function () {
			// get "structure" data attributes and add index+1 value 
			var dattr = $(this).data('structure') + num;

			// update "id" and "name" attributes
			$(this).attr({
				'id': dattr,
				'name': dattr
			});

			// if the field type is "number"...
			if ($(this).attr('type') == 'number') {
				// set value to zero
				$(this).val('0');
			} else {
				// clear the value...
				$(this).val('');
			}

			// update "counter" field
			$('#txtCount').val( $('.repeatable').length );
		});

		// run these... because...
		destroy();
		updateRemoveLinks();
	});
	
	// on entering data TAM name, if user selects one of the names from the pre-defined datalist
	$('#txtDataTamName').on('input', function () {
		var option = $('#lstDataTam option[value="' + $(this).val() + '"]');
		
		// grab the custom data attributes from the data list for that option, and auto-populate the other fields with the data
		$('#txtDataTamTitle').val( option.data('title') );
		$('#txtDataTamEmail').val( option.data('email') );
		$('#txtDataTamPhone').val( option.data('phone') );
	});
	
	// jquery date picker
	$('#txtTodaysDate').datepicker({
        changeMonth: true,
        changeYear: true,
        dateFormat: 'mm/dd/yy',
        defaultDate: new Date()
    });
	
	// when user selects a service term
	$('#ddlServiceTerm').change(function(){
		var value = $(this).val();
		
		// update the header in the table with the value, and modify css appropriately
		if(value != ''){
			$('#spnSelectedServiceTerm').css({
				'background-color': 'inherit'
			}).html(value);
		} else {
			$('#spnSelectedServiceTerm').css({
				'background-color': '#FF0'
			}).html('???');
		}
	});
	
	// on form submit
	$('#frmInput').submit(function(e) {
		e.preventDefault();
		
		// validate
		// 24 chars long for phone number
		
		var v = {
			'date': {
				'today': $('#txtTodaysDate').val(),
				'expiration': ''
			},
			'data_tam': {
				'name': $('#txtDataTamName').val(),
				'title': $('#txtDataTamTitle').val(),
				'phone': $('#txtDataTamPhone').val(),
				'email': $('#txtDataTamEmail').val()
			},
			'customer': {
				'name': $('#txtCustomerName').val(),
				'logo': '&nbsp;'
			},
			'service_term': $('#ddlServiceTerm option:selected').val(),
			'pricing': {
				'count': $('#txtCount').val(),
				'items': [],
				'total': {
					'current_mrc': 0,
					'mrc_x_months': 0,
					'nrc': 0
				},
				'difference': 0
			}
		};
		
		var d1 = new Date(v.date.today);
		var d2 = new Date(d1.setDate(d1.getDate() + 30));

		v.date.expiration = (d2.getMonth() + 1) + '/' + d2.getDate() + '/' + d2.getFullYear();
		
		for (var x = 0, i = 1; x < v.pricing.count; x++, i++) {
			v.pricing.items[x] = {
				'location': $('#txtLocation' + i).val(),
				'description': $('#txtDescription' + i).val(),
				'bandwidth': $('#txtBandwidth' + i).val(),
				'current_mrc': parseFloat($('#txtCurrentMRC' + i).val()),
				'mrc_x_months': parseFloat($('#txtMRCXMonths' + i).val()),
				'nrc': parseFloat($('#txtNRC' + i).val())
			}
			
			v.pricing.total.current_mrc += v.pricing.items[x].current_mrc;
			v.pricing.total.mrc_x_months += v.pricing.items[x].mrc_x_months;
			v.pricing.total.nrc += v.pricing.items[x].nrc;
		}
		
		v.pricing.difference = v.pricing.total.mrc_x_months - v.pricing.total.current_mrc;
		
		var output = '<header><div class="row"><div class="one-half column center"><!-- <img src="https://141nh047iozd1l75s22eer06-wpengine.netdna-ssl.com/wp-content/uploads/2014/05/Fake-Bright-Green-Logo_Horz-v2-760.png" alt="Customer Logo"> --> &nbsp; </div><div class="one-half column center"><img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTRHYS0wR0wmI42bCehl7bJ_vHuPckAX_ejfrb2dWUKE75z_iZo6A" alt="' + v.customer.name + ' Logo"></div></div><div class="row"><div class="one-half column center"><table><tbody><tr><th>Customer Name:</th><td>' + v.customer.name + '</td></tr><tr><th>Service Term:</th><td>' + v.service_term + '</td></tr></tbody></table></div><div class="one-half column center"><p class="red">Budgetary Quote</p><table><caption>Quote Valid for 30 Days</caption><tbody><tr><th>Date:</th><td>' + v.date.today + '</td></tr><tr><th>Prepared By:</th><td>' + v.data_tam.name + '</td></tr><tr><th>Title:</th><td>' + v.data_tam.title + '</td></tr><tr><th>Telephone:</th><td>' + v.data_tam.phone + '</td></tr><tr><th>Email:</th><td><a href="mailto:' + v.data_tam.email + '">' + v.data_tam.email + '</a></td></tr></tbody></table></div></div></header><section><table id="tblFinalBreakdown"><thead><tr><th>Location</th><th>Product Description</th><th>Bandwidth</th><th>Current MRC</th><th>MRC 36 Mos</th><th>NRC</th></tr></thead><tbody>';
		
		for (var j = 0; j <= (v.pricing.items.length - 1); j++) {
			output += '<tr><td>' + v.pricing.items[j].location + '</td><td>' + v.pricing.items[j].description + '</td><td>' + v.pricing.items[j].bandwidth + '</td><td class="currency">' + v.pricing.items[j].current_mrc + '</td><td class="currency">' + v.pricing.items[j].mrc_x_months + '</td><td class="currency">' + v.pricing.items[j].nrc + '</td></tr>';
		}

output += '</tbody><tfoot><tr><td colspan="3" class="invisible"></td><td class="currency">' + v.pricing.total.current_mrc + '</td><td class="currency">' + v.pricing.total.mrc_x_months + '</td><td class="currency">' + v.pricing.total.nrc + '</td></tr><tr><td colspan="4" class="invisible">Current vs. Net MRC Change:</td><td class="currency">' + v.pricing.difference + '</td><td class="invisible"></td></tr></tfoot></table></section><section><ul><li>MRC = Monthly Recurring Charges</li><li>NRC = Non-Recurring Charges</li><li>Totals do not include any taxes, fees, or surcharges.</li><li>Current Term (' + v.service_term + ' months) expires <strong>' + v.date.expiration + '</strong>.</li><li>Ethernet equipment fees: $24.95 per month, per site.</li></ul></section><footer><p><small>Proprietary and Confidential</small></p></footer>';
		
		$('#artOutput').show();
		
		$('#artOutput').html(output);
		
		$('#artOutput').prepend('<input id="btnPrint" type="button" class="button-primary no-print" value="Print">');
		
		$('#btnPrint').click(function(){
			window.print();
		});
		
		window.location.hash = '#artOutput';
	});
});

// to do:
// * format dollar amounts for 2 decimal places
// * zero dollar amounts represented as a hyphen

Comments