diff --git a/hosting/static/hosting/css/order.css b/hosting/static/hosting/css/order.css index bdd124c9..3e916cdd 100644 --- a/hosting/static/hosting/css/order.css +++ b/hosting/static/hosting/css/order.css @@ -20,3 +20,19 @@ .order-detail-container .table > tbody > tr > .thick-line { border-top: 2px solid; } + +.order-detail-container .dashboard-title-thin { + margin-top: 0; +} + +.order-detail-container .dashboard-title-thin .un-icon { + margin-top: -6px; +} + +.order-detail-container .dashboard-title-thin .svg-img { + height: 20px; + margin-right: 5px; + margin-top: -2px; + width: 20px; + margin-top: -6px; +} \ No newline at end of file diff --git a/hosting/static/hosting/js/html2pdf.js b/hosting/static/hosting/js/html2pdf.js new file mode 100644 index 00000000..45ae5b0c --- /dev/null +++ b/hosting/static/hosting/js/html2pdf.js @@ -0,0 +1,387 @@ +/** + * @license + * + * MIT License + * + * Copyright (c) 2017 Erik Koopmans + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Generate a PDF from an HTML element or string using html2canvas and jsPDF. + * + * @param {Element|string} source The source element or HTML string. + * @param {Object=} opt An object of optional settings: 'margin', 'filename', + * 'image' ('type' and 'quality'), and 'html2canvas' / 'jspdf', which are + * sent as settings to their corresponding functions. + */ +var html2pdf = (function(html2canvas, jsPDF) { + + /* ---------- MAIN FUNCTION ---------- */ + + var html2pdf = function(source, opt) { + // Handle input. + opt = objType(opt) === 'object' ? opt : {}; + var source = html2pdf.parseInput(source, opt); + + // Determine the PDF page size. + var pageSize = jsPDF.getPageSize(opt.jsPDF); + pageSize.inner = { + width: pageSize.width - opt.margin[1] - opt.margin[3], + height: pageSize.height - opt.margin[0] - opt.margin[2] + }; + pageSize.inner.ratio = pageSize.inner.height / pageSize.inner.width; + + // Copy the source element into a PDF-styled container div. + var container = html2pdf.makeContainer(source, pageSize); + var overlay = container.parentElement; + + // Get the locations of all hyperlinks. + if (opt.enableLinks) { + // Find all anchor tags and get the container's bounds for reference. + opt.links = []; + var links = container.querySelectorAll('a'); + var containerRect = unitConvert(container.getBoundingClientRect(), pageSize.k); + + // Treat each client rect as a separate link (for text-wrapping). + Array.prototype.forEach.call(links, function(link) { + var clientRects = link.getClientRects(); + for (var i=0; i<clientRects.length; i++) { + var clientRect = unitConvert(clientRects[i], pageSize.k); + clientRect.left -= containerRect.left; + clientRect.top -= containerRect.top; + opt.links.push({ el: link, clientRect: clientRect }); + } + }); + } + + // Render the canvas and pass the result to makePDF. + var onRendered = opt.html2canvas.onrendered || function() {}; + opt.html2canvas.onrendered = function(canvas) { + onRendered(canvas); + document.body.removeChild(overlay); + html2pdf.makePDF(canvas, pageSize, opt); + } + html2canvas(container, opt.html2canvas); + }; + + html2pdf.parseInput = function(source, opt) { + // Parse the opt object. + opt.jsPDF = opt.jsPDF || {}; + opt.html2canvas = opt.html2canvas || {}; + opt.filename = opt.filename && objType(opt.filename) === 'string' ? opt.filename : 'file.pdf'; + opt.enableLinks = opt.hasOwnProperty('enableLinks') ? opt.enableLinks : true; + opt.image = opt.image || {}; + opt.image.type = opt.image.type || 'jpeg'; + opt.image.quality = opt.image.quality || 0.95; + + // Parse the margin property of the opt object. + switch (objType(opt.margin)) { + case 'undefined': + opt.margin = 0; + case 'number': + opt.margin = [opt.margin, opt.margin, opt.margin, opt.margin]; + break; + case 'array': + if (opt.margin.length === 2) { + opt.margin = [opt.margin[0], opt.margin[1], opt.margin[0], opt.margin[1]]; + } + if (opt.margin.length === 4) { + break; + } + default: + throw 'Invalid margin array.'; + } + + // Parse the source element/string. + if (!source) { + throw 'Missing source element or string.'; + } else if (objType(source) === 'string') { + source = createElement('div', { innerHTML: source }); + } else if (objType(source) === 'element') { + source = cloneNode(source, opt.html2canvas.javascriptEnabled); + } else { + throw 'Invalid source - please specify an HTML Element or string.'; + } + + // Return the parsed input (opt is modified in-place, no need to return). + return source; + }; + + html2pdf.makeContainer = function(source, pageSize) { + // Define the CSS styles for the container and its overlay parent. + var overlayCSS = { + position: 'fixed', overflow: 'hidden', zIndex: 1000, + left: 0, right: 0, bottom: 0, top: 0, + backgroundColor: 'rgba(0,0,0,0.8)' + }; + var containerCSS = { + position: 'absolute', width: pageSize.inner.width + pageSize.unit, + left: 0, right: 0, top: 0, height: 'auto', margin: 'auto', + backgroundColor: 'white' + }; + + // Set the overlay to hidden (could be changed in the future to provide a print preview). + overlayCSS.opacity = 0; + + // Create and attach the elements. + var overlay = createElement('div', { className: 'html2pdf__overlay', style: overlayCSS }); + var container = createElement('div', { className: 'html2pdf__container', style: containerCSS }); + container.appendChild(source); + overlay.appendChild(container); + document.body.appendChild(overlay); + + // Enable page-breaks. + var pageBreaks = source.querySelectorAll('.html2pdf__page-break'); + var pxPageHeight = pageSize.inner.height * pageSize.k / 72 * 96; + Array.prototype.forEach.call(pageBreaks, function(el) { + el.style.display = 'block'; + var clientRect = el.getBoundingClientRect(); + el.style.height = pxPageHeight - (clientRect.top % pxPageHeight) + 'px'; + }, this); + + // Return the container. + return container; + }; + + html2pdf.makePDF = function(canvas, pageSize, opt) { + // Calculate the number of pages. + var ctx = canvas.getContext('2d'); + var pxFullHeight = canvas.height; + var pxPageHeight = Math.floor(canvas.width * pageSize.inner.ratio); + var nPages = Math.ceil(pxFullHeight / pxPageHeight); + + // Create a one-page canvas to split up the full image. + var pageCanvas = document.createElement('canvas'); + var pageCtx = pageCanvas.getContext('2d'); + var pageHeight = pageSize.inner.height; + pageCanvas.width = canvas.width; + pageCanvas.height = pxPageHeight; + + // Initialize the PDF. + var pdf = new jsPDF(opt.jsPDF); + + for (var page=0; page<nPages; page++) { + // Trim the final page to reduce file size. + if (page === nPages-1) { + pageCanvas.height = pxFullHeight % pxPageHeight; + pageHeight = pageCanvas.height * pageSize.inner.width / pageCanvas.width; + } + + // Display the page. + var w = pageCanvas.width; + var h = pageCanvas.height; + pageCtx.fillStyle = 'white'; + pageCtx.fillRect(0, 0, w, h); + pageCtx.drawImage(canvas, 0, page*pxPageHeight, w, h, 0, 0, w, h); + + // Add the page to the PDF. + if (page) pdf.addPage(); + var imgData = pageCanvas.toDataURL('image/' + opt.image.type, opt.image.quality); + pdf.addImage(imgData, opt.image.type, opt.margin[1], opt.margin[0], + pageSize.inner.width, pageHeight); + + // Add hyperlinks. + if (opt.enableLinks) { + var pageTop = page * pageSize.inner.height; + opt.links.forEach(function(link) { + if (link.clientRect.top > pageTop && link.clientRect.top < pageTop + pageSize.inner.height) { + var left = opt.margin[1] + link.clientRect.left; + var top = opt.margin[0] + link.clientRect.top - pageTop; + pdf.link(left, top, link.clientRect.width, link.clientRect.height, { url: link.el.href }); + } + }); + } + } + + // Finish the PDF. + pdf.save( opt.filename ); + } + + + /* ---------- UTILS ---------- */ + + // Determine the type of a variable/object. + var objType = function(obj) { + if (typeof obj === 'undefined') return 'undefined'; + else if (typeof obj === 'string' || obj instanceof String) return 'string'; + else if (typeof obj === 'number' || obj instanceof Number) return 'number'; + else if (!!obj && obj.constructor === Array) return 'array'; + else if (obj && obj.nodeType === 1) return 'element'; + else if (typeof obj === 'object') return 'object'; + else return 'unknown'; + }; + + // Create an HTML element with optional className, innerHTML, and style. + var createElement = function(tagName, opt) { + var el = document.createElement(tagName); + if (opt.className) el.className = opt.className; + if (opt.innerHTML) { + el.innerHTML = opt.innerHTML; + var scripts = el.getElementsByTagName('script'); + for (var i = scripts.length; i-- > 0; null) { + scripts[i].parentNode.removeChild(scripts[i]); + } + } + for (var key in opt.style) { + el.style[key] = opt.style[key]; + } + return el; + }; + + // Deep-clone a node and preserve contents/properties. + var cloneNode = function(node, javascriptEnabled) { + // Recursively clone the node. + var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false); + for (var child = node.firstChild; child; child = child.nextSibling) { + if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') { + clone.appendChild(cloneNode(child, javascriptEnabled)); + } + } + + if (node.nodeType === 1) { + // Preserve contents/properties of special nodes. + if (node.nodeName === 'CANVAS') { + clone.width = node.width; + clone.height = node.height; + clone.getContext('2d').drawImage(node, 0, 0); + } else if (node.nodeName === 'TEXTAREA' || node.nodeName === 'SELECT') { + clone.value = node.value; + } + + // Preserve the node's scroll position when it loads. + clone.addEventListener('load', function() { + clone.scrollTop = node.scrollTop; + clone.scrollLeft = node.scrollLeft; + }, true); + } + + // Return the cloned node. + return clone; + } + + // Convert units using the conversion value 'k' from jsPDF. + var unitConvert = function(obj, k) { + var newObj = {}; + for (var key in obj) { + newObj[key] = obj[key] * 72 / 96 / k; + } + return newObj; + }; + + // Get dimensions of a PDF page, as determined by jsPDF. + jsPDF.getPageSize = function(orientation, unit, format) { + // Decode options object + if (typeof orientation === 'object') { + var options = orientation; + orientation = options.orientation; + unit = options.unit || unit; + format = options.format || format; + } + + // Default options + unit = unit || 'mm'; + format = format || 'a4'; + orientation = ('' + (orientation || 'P')).toLowerCase(); + var format_as_string = ('' + format).toLowerCase(); + + // Size in pt of various paper formats + pageFormats = { + 'a0' : [2383.94, 3370.39], 'a1' : [1683.78, 2383.94], + 'a2' : [1190.55, 1683.78], 'a3' : [ 841.89, 1190.55], + 'a4' : [ 595.28, 841.89], 'a5' : [ 419.53, 595.28], + 'a6' : [ 297.64, 419.53], 'a7' : [ 209.76, 297.64], + 'a8' : [ 147.40, 209.76], 'a9' : [ 104.88, 147.40], + 'a10' : [ 73.70, 104.88], 'b0' : [2834.65, 4008.19], + 'b1' : [2004.09, 2834.65], 'b2' : [1417.32, 2004.09], + 'b3' : [1000.63, 1417.32], 'b4' : [ 708.66, 1000.63], + 'b5' : [ 498.90, 708.66], 'b6' : [ 354.33, 498.90], + 'b7' : [ 249.45, 354.33], 'b8' : [ 175.75, 249.45], + 'b9' : [ 124.72, 175.75], 'b10' : [ 87.87, 124.72], + 'c0' : [2599.37, 3676.54], 'c1' : [1836.85, 2599.37], + 'c2' : [1298.27, 1836.85], 'c3' : [ 918.43, 1298.27], + 'c4' : [ 649.13, 918.43], 'c5' : [ 459.21, 649.13], + 'c6' : [ 323.15, 459.21], 'c7' : [ 229.61, 323.15], + 'c8' : [ 161.57, 229.61], 'c9' : [ 113.39, 161.57], + 'c10' : [ 79.37, 113.39], 'dl' : [ 311.81, 623.62], + 'letter' : [612, 792], + 'government-letter' : [576, 756], + 'legal' : [612, 1008], + 'junior-legal' : [576, 360], + 'ledger' : [1224, 792], + 'tabloid' : [792, 1224], + 'credit-card' : [153, 243] + }; + + // Unit conversion + switch (unit) { + case 'pt': k = 1; break; + case 'mm': k = 72 / 25.4; break; + case 'cm': k = 72 / 2.54; break; + case 'in': k = 72; break; + case 'px': k = 72 / 96; break; + case 'pc': k = 12; break; + case 'em': k = 12; break; + case 'ex': k = 6; break; + default: + throw ('Invalid unit: ' + unit); + } + + // Dimensions are stored as user units and converted to points on output + if (pageFormats.hasOwnProperty(format_as_string)) { + pageHeight = pageFormats[format_as_string][1] / k; + pageWidth = pageFormats[format_as_string][0] / k; + } else { + try { + pageHeight = format[1]; + pageWidth = format[0]; + } catch (err) { + throw new Error('Invalid format: ' + format); + } + } + + // Handle page orientation + if (orientation === 'p' || orientation === 'portrait') { + orientation = 'p'; + if (pageWidth > pageHeight) { + tmp = pageWidth; + pageWidth = pageHeight; + pageHeight = tmp; + } + } else if (orientation === 'l' || orientation === 'landscape') { + orientation = 'l'; + if (pageHeight > pageWidth) { + tmp = pageWidth; + pageWidth = pageHeight; + pageHeight = tmp; + } + } else { + throw('Invalid orientation: ' + orientation); + } + + // Return information (k is the unit conversion ratio from pts) + var info = { 'width': pageWidth, 'height': pageHeight, 'unit': unit, 'k': k }; + return info; + }; + + + // Expose the html2pdf function. + return html2pdf; +}(html2canvas, jsPDF)); diff --git a/hosting/templates/hosting/order_detail.html b/hosting/templates/hosting/order_detail.html index 3d1ffd35..8278db45 100644 --- a/hosting/templates/hosting/order_detail.html +++ b/hosting/templates/hosting/order_detail.html @@ -5,7 +5,7 @@ {% block content %} -<div class="order-detail-container"> +<div id="order-detail" class="order-detail-container"> {% if messages %} <div class="alert alert-warning"> {% for message in messages %} @@ -14,17 +14,20 @@ </div> {% endif %} {% if not error %} - <h1 class="dashboard-title-thin"> - {% blocktrans %}{{page_header_text|default:'Invoice'}}{% endblocktrans %} - </h1> + <div class="dashboard-container-head"> + <h1 class="dashboard-title-thin"> + <img src="{% static 'hosting/img/billing.svg' %}" class="un-icon">{% blocktrans with page_header_text=page_header_text|default:"Invoice" %}{{page_header_text}}{% endblocktrans %} + </h1> + + <button></button> + <img src="{% static 'hosting/img/icon-pdf.svg' %}" class="svg-img pull-right"> + <img src="{% static 'hosting/img/icon-print.svg' %}" class="svg-img pull-right"> + </div> <div class="invoice-title"> - <h3 class="pull-right"> - {% if order %} - {% trans "Order #"%} {{order.id}} - {% endif %} + <h3> + {% trans "Order #"%} {{order.id}} </h3> </div> - <hr> <div class="row"> <div class="col-xs-12 col-md-6 pull-right order-confirm-date"> <address> @@ -202,5 +205,11 @@ }; </script> +<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.3.5/jspdf.min.js"></script> +<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script> +<script src="{% static 'hosting/js/html2pdf.js' %}"></script> +<script> + html2pdf(document.getElementById('order-detail')); +</script> {%endblock%}