Skip to content

Instantly share code, notes, and snippets.

@damonp
Last active September 14, 2023 08:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save damonp/e6dcef7dd6519d368c45 to your computer and use it in GitHub Desktop.
Save damonp/e6dcef7dd6519d368c45 to your computer and use it in GitHub Desktop.
HTML5 Editable Invoice Example
<!--
Credit: http://www.jonathantneal.com/blog/the-html5-invoice/
Rolled into single file for transportability.
-->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice: #</title>
<style>
/* reset */
*
{
border: 0;
box-sizing: content-box;
color: inherit;
font-family: inherit;
font-size: inherit;
font-style: inherit;
font-weight: inherit;
line-height: inherit;
list-style: none;
margin: 0;
padding: 0;
text-decoration: none;
vertical-align: top;
}
/* content editable */
*[contenteditable] { border-radius: 0.25em; min-width: 1em; outline: 0; }
*[contenteditable] { cursor: pointer; }
*[contenteditable]:hover, *[contenteditable]:focus, td:hover *[contenteditable], td:focus *[contenteditable], img.hover { background: #DEF; box-shadow: 0 0 1em 0.5em #DEF; }
span[contenteditable] { display: inline-block; }
/* heading */
h1 { font: bold 100% sans-serif; letter-spacing: 0.5em; text-align: center; text-transform: uppercase; }
/* table */
table { font-size: 75%; table-layout: fixed; width: 100%; }
table { border-collapse: separate; border-spacing: 2px; }
th, td { border-width: 1px; padding: 0.5em; position: relative; text-align: left; }
th, td { border-radius: 0.25em; border-style: solid; }
th { background: #EEE; border-color: #BBB; }
td { border-color: #DDD; }
/* page */
html { font: 16px/1 'Open Sans', sans-serif; overflow: auto; padding: 0.5in; }
html { background: #999; cursor: default; }
body { box-sizing: border-box; height: 11in; margin: 0 auto; overflow: hidden; padding: 0.5in; width: 8.5in; }
body { background: #FFF; border-radius: 1px; box-shadow: 0 0 1in -0.25in rgba(0, 0, 0, 0.5); }
/* header */
header { margin: 0 0 3em; }
header:after { clear: both; content: ""; display: table; }
header h1 { background: #000; border-radius: 0.25em; color: #FFF; margin: 0 0 1em; padding: 0.5em 0; }
header address { float: left; font-size: 75%; font-style: normal; line-height: 1.25; margin: 0 1em 1em 0; }
header address p { margin: 0 0 0.25em; }
header span, header img { display: block; float: right; }
header span { margin: 0 0 1em 1em; max-height: 25%; max-width: 60%; position: relative; }
header img { max-height: 100%; max-width: 100%; }
header input { cursor: pointer; -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; height: 100%; left: 0; opacity: 0; position: absolute; top: 0; width: 100%; }
/* article */
article, article address, table.meta, table.inventory { margin: 0 0 3em; }
article:after { clear: both; content: ""; display: table; }
article h1 { clip: rect(0 0 0 0); position: absolute; }
article address { float: left; font-size: 125%; font-weight: bold; }
/* table meta & balance */
table.meta, table.balance { float: right; width: 36%; }
table.meta:after, table.balance:after { clear: both; content: ""; display: table; }
/* table meta */
table.meta th { width: 40%; }
table.meta td { width: 60%; }
/* table items */
table.inventory { clear: both; width: 100%; }
table.inventory th { font-weight: bold; text-align: center; }
table.inventory td:nth-child(1) { width: 26%; }
table.inventory td:nth-child(2) { width: 38%; }
table.inventory td:nth-child(3) { text-align: right; width: 12%; }
table.inventory td:nth-child(4) { text-align: right; width: 12%; }
table.inventory td:nth-child(5) { text-align: right; width: 12%; }
/* table balance */
table.balance th, table.balance td { width: 50%; }
table.balance td { text-align: right; }
/* aside */
aside h1 { border: none; border-width: 0 0 1px; margin: 0 0 1em; }
aside h1 { border-color: #999; border-bottom-style: solid; }
/* javascript */
.add, .cut
{
border-width: 1px;
display: block;
font-size: .8rem;
padding: 0.25em 0.5em;
float: left;
text-align: center;
width: 0.6em;
}
.add, .cut
{
background: #9AF;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
background-image: -moz-linear-gradient(#00ADEE 5%, #0078A5 100%);
background-image: -webkit-linear-gradient(#00ADEE 5%, #0078A5 100%);
border-radius: 0.5em;
border-color: #0076A3;
color: #FFF;
cursor: pointer;
font-weight: bold;
text-shadow: 0 -1px 2px rgba(0,0,0,0.333);
}
.add { margin: -2.5em 0 0; }
.add:hover { background: #00ADEE; }
.cut { opacity: 0; position: absolute; top: 0; left: -1.5em; }
.cut { -webkit-transition: opacity 100ms ease-in; }
tr:hover .cut { opacity: 1; }
@media print {
* { -webkit-print-color-adjust: exact; }
html { background: none; padding: 0; }
body { box-shadow: none; margin: 0; }
span:empty { display: none; }
.add, .cut { display: none; }
}
@page { margin: 0; }
</style>
<link rel="license" href="http://www.opensource.org/licenses/mit-license/">
</head>
<body>
<header>
<h1>Invoice</h1>
<address contenteditable>
<p>Acme Widgets</p>
<p>123 Some Street<br>SomeCity, TX 10101</p>
<p>{###) ###-####</p>
</address>
<span><img alt="" src="logo.png"><input type="file" accept="image/*"></span>
</header>
<article>
<h1>Recipient</h1>
<address contenteditable>
<p>Some Company<br>c/o Some Guy</p>
</address>
<table class="meta">
<tr>
<th><span contenteditable>Invoice #</span></th>
<td><span contenteditable>101001</span></td>
</tr>
<tr>
<th><span contenteditable>Date</span></th>
<td><span contenteditable><?php echo date('Y-m-d'); ?></span></td>
</tr>
<tr>
<th><span contenteditable>Amount Due</span></th>
<td><span id="prefix" contenteditable>$</span><span>600.00</span></td>
</tr>
</table>
<table class="inventory">
<thead>
<tr>
<th><span contenteditable>Item</span></th>
<th><span contenteditable>Description</span></th>
<th><span contenteditable>Rate</span></th>
<th><span contenteditable>Quantity</span></th>
<th><span contenteditable>Price</span></th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="cut">-</a><span contenteditable>Front End Consultation</span></td>
<td><span contenteditable>Experience Review</span></td>
<td><span data-prefix>$</span><span contenteditable>150.00</span></td>
<td><span contenteditable>4</span></td>
<td><span data-prefix>$</span><span>600.00</span></td>
</tr>
</tbody>
</table>
<a class="add">+</a>
<table class="balance">
<tr>
<th><span contenteditable>Total</span></th>
<td><span data-prefix>$</span><span>600.00</span></td>
</tr>
<tr>
<th><span contenteditable>Amount Paid</span></th>
<td><span data-prefix>$</span><span contenteditable>0.00</span></td>
</tr>
<tr>
<th><span contenteditable>Balance Due</span></th>
<td><span data-prefix>$</span><span>600.00</span></td>
</tr>
</table>
</article>
<aside>
<h1><span contenteditable>Additional Notes</span></h1>
<div contenteditable>
<p>A finance charge of 1.5% will be made on unpaid balances after 30 days.</p>
</div>
</aside>
<script>
/* Shivving (IE8 is not supported, but at least it won't look as awful)
/* ========================================================================== */
(function (document) {
var
head = document.head = document.getElementsByTagName('head')[0] || document.documentElement,
elements = 'article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output picture progress section summary time video x'.split(' '),
elementsLength = elements.length,
elementsIndex = 0,
element;
while (elementsIndex < elementsLength) {
element = document.createElement(elements[++elementsIndex]);
}
element.innerHTML = 'x<style>' +
'article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}' +
'audio[controls],canvas,video{display:inline-block}' +
'[hidden],audio{display:none}' +
'mark{background:#FF0;color:#000}' +
'</style>';
return head.insertBefore(element.lastChild, head.firstChild);
})(document);
/* Prototyping
/* ========================================================================== */
(function (window, ElementPrototype, ArrayPrototype, polyfill) {
function NodeList() { [polyfill] }
NodeList.prototype.length = ArrayPrototype.length;
ElementPrototype.matchesSelector = ElementPrototype.matchesSelector ||
ElementPrototype.mozMatchesSelector ||
ElementPrototype.msMatchesSelector ||
ElementPrototype.oMatchesSelector ||
ElementPrototype.webkitMatchesSelector ||
function matchesSelector(selector) {
return ArrayPrototype.indexOf.call(this.parentNode.querySelectorAll(selector), this) > -1;
};
ElementPrototype.ancestorQuerySelectorAll = ElementPrototype.ancestorQuerySelectorAll ||
ElementPrototype.mozAncestorQuerySelectorAll ||
ElementPrototype.msAncestorQuerySelectorAll ||
ElementPrototype.oAncestorQuerySelectorAll ||
ElementPrototype.webkitAncestorQuerySelectorAll ||
function ancestorQuerySelectorAll(selector) {
for (var cite = this, newNodeList = new NodeList; cite = cite.parentElement;) {
if (cite.matchesSelector(selector)) ArrayPrototype.push.call(newNodeList, cite);
}
return newNodeList;
};
ElementPrototype.ancestorQuerySelector = ElementPrototype.ancestorQuerySelector ||
ElementPrototype.mozAncestorQuerySelector ||
ElementPrototype.msAncestorQuerySelector ||
ElementPrototype.oAncestorQuerySelector ||
ElementPrototype.webkitAncestorQuerySelector ||
function ancestorQuerySelector(selector) {
return this.ancestorQuerySelectorAll(selector)[0] || null;
};
})(this, Element.prototype, Array.prototype);
/* Helper Functions
/* ========================================================================== */
function generateTableRow() {
var emptyColumn = document.createElement('tr');
emptyColumn.innerHTML = '<td><a class="cut">-</a><span contenteditable></span></td>' +
'<td><span contenteditable></span></td>' +
'<td><span data-prefix>$</span><span contenteditable>0.00</span></td>' +
'<td><span contenteditable>0</span></td>' +
'<td><span data-prefix>$</span><span>0.00</span></td>';
return emptyColumn;
}
function parseFloatHTML(element) {
return parseFloat(element.innerHTML.replace(/[^\d\.\-]+/g, '')) || 0;
}
function parsePrice(number) {
return number.toFixed(2).replace(/(\d)(?=(\d\d\d)+([^\d]|$))/g, '$1,');
}
/* Update Number
/* ========================================================================== */
function updateNumber(e) {
var
activeElement = document.activeElement,
value = parseFloat(activeElement.innerHTML),
wasPrice = activeElement.innerHTML == parsePrice(parseFloatHTML(activeElement));
if (!isNaN(value) && (e.keyCode == 38 || e.keyCode == 40 || e.wheelDeltaY)) {
e.preventDefault();
value += e.keyCode == 38 ? 1 : e.keyCode == 40 ? -1 : Math.round(e.wheelDelta * 0.025);
value = Math.max(value, 0);
activeElement.innerHTML = wasPrice ? parsePrice(value) : value;
}
updateInvoice();
}
/* Update Invoice
/* ========================================================================== */
function updateInvoice() {
var total = 0;
var cells, price, total, a, i;
// update inventory cells
// ======================
for (var a = document.querySelectorAll('table.inventory tbody tr'), i = 0; a[i]; ++i) {
// get inventory row cells
cells = a[i].querySelectorAll('span:last-child');
// set price as cell[2] * cell[3]
price = parseFloatHTML(cells[2]) * parseFloatHTML(cells[3]);
// add price to total
total += price;
// set row total
cells[4].innerHTML = price;
}
// update balance cells
// ====================
// get balance cells
cells = document.querySelectorAll('table.balance td:last-child span:last-child');
// set total
cells[0].innerHTML = total;
// set balance and meta balance
cells[2].innerHTML = document.querySelector('table.meta tr:last-child td:last-child span:last-child').innerHTML = parsePrice(total - parseFloatHTML(cells[1]));
// update prefix formatting
// ========================
var prefix = document.querySelector('#prefix').innerHTML;
for (a = document.querySelectorAll('[data-prefix]'), i = 0; a[i]; ++i) a[i].innerHTML = prefix;
// update price formatting
// =======================
for (a = document.querySelectorAll('span[data-prefix] + span'), i = 0; a[i]; ++i) if (document.activeElement != a[i]) a[i].innerHTML = parsePrice(parseFloatHTML(a[i]));
}
/* On Content Load
/* ========================================================================== */
function onContentLoad() {
updateInvoice();
var
input = document.querySelector('input'),
image = document.querySelector('img');
function onClick(e) {
var element = e.target.querySelector('[contenteditable]'), row;
element && e.target != document.documentElement && e.target != document.body && element.focus();
if (e.target.matchesSelector('.add')) {
document.querySelector('table.inventory tbody').appendChild(generateTableRow());
}
else if (e.target.className == 'cut') {
row = e.target.ancestorQuerySelector('tr');
row.parentNode.removeChild(row);
}
updateInvoice();
}
function onEnterCancel(e) {
e.preventDefault();
image.classList.add('hover');
}
function onLeaveCancel(e) {
e.preventDefault();
image.classList.remove('hover');
}
function onFileInput(e) {
image.classList.remove('hover');
var
reader = new FileReader(),
files = e.dataTransfer ? e.dataTransfer.files : e.target.files,
i = 0;
reader.onload = onFileLoad;
while (files[i]) reader.readAsDataURL(files[i++]);
}
function onFileLoad(e) {
var data = e.target.result;
image.src = data;
}
if (window.addEventListener) {
document.addEventListener('click', onClick);
document.addEventListener('mousewheel', updateNumber);
document.addEventListener('keydown', updateNumber);
document.addEventListener('keydown', updateInvoice);
document.addEventListener('keyup', updateInvoice);
input.addEventListener('focus', onEnterCancel);
input.addEventListener('mouseover', onEnterCancel);
input.addEventListener('dragover', onEnterCancel);
input.addEventListener('dragenter', onEnterCancel);
input.addEventListener('blur', onLeaveCancel);
input.addEventListener('dragleave', onLeaveCancel);
input.addEventListener('mouseout', onLeaveCancel);
input.addEventListener('drop', onFileInput);
input.addEventListener('change', onFileInput);
}
}
window.addEventListener && document.addEventListener('DOMContentLoaded', onContentLoad);
</script>
</body>
</html>
@Nada-86
Copy link

Nada-86 commented Sep 14, 2023

WOOOWWW!!!
it's the first time I saw something perfect like that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment