From 5ef83ab236c633528ffe992b3b73aa86fe86090f Mon Sep 17 00:00:00 2001 From: Lewis Cowles Date: Wed, 22 Apr 2020 17:31:48 +0100 Subject: Export Board to Zip file * Extracts Card covers * Labels * Re-works some CSS & HTML * Produces deployable assets (minus WebFonts) --- client/components/sidebar/sidebar.jade | 6 +- client/components/sidebar/sidebar.js | 7 ++ client/lib/exportHTML.js | 206 +++++++++++++++++++++++++++++++++ package-lock.json | 34 ++++++ package.json | 1 + 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 client/lib/exportHTML.js diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 7d637142..04f2a8c2 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -363,7 +363,7 @@ template(name="boardMenuPopup") template(name="exportBoard") ul.pop-over-list li - a(href="{{exportUrl}}", download="{{exportJsonFilename}}") + a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}") i.fa.fa-share-alt | {{_ 'export-board-json'}} li @@ -374,6 +374,10 @@ template(name="exportBoard") a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}") i.fa.fa-share-alt | {{_ 'export-board-tsv'}} + li + a.html-export-board + i.fa.fa-archive + | {{_ 'export-board-html'}} template(name="labelsWidget") .board-widget.board-widget-labels diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 2c1cfd75..0e535041 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -463,6 +463,13 @@ BlazeComponent.extendComponent({ }, }).register('exportBoardPopup'); +Template.exportBoard.events({ + 'click .html-export-board': async event => { + event.preventDefault(); + await ExportHtml(Popup)(); + } +}); + Template.labelsWidget.events({ 'click .js-label': Popup.open('editLabel'), 'click .js-add-label': Popup.open('createLabel'), diff --git a/client/lib/exportHTML.js b/client/lib/exportHTML.js new file mode 100644 index 00000000..fe15b6aa --- /dev/null +++ b/client/lib/exportHTML.js @@ -0,0 +1,206 @@ +const JSZip = require('jszip'); + +window.ExportHtml = (Popup) => { + const saveAs = function(blob, filename) { + let dl = document.createElement('a'); + dl.href = window.URL.createObjectURL(blob); + dl.onclick = event => document.body.removeChild(event.target); + dl.style.display = 'none'; + dl.target = '_blank'; + dl.download = filename; + document.body.appendChild(dl); + dl.click(); + }; + + const asyncForEach = async function (array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } + }; + + const getPageHtmlString = () => { + return `${ + window.document.querySelector('html').outerHTML + }`; + }; + + const removeAnchors = htmlString => { + const replaceOpenAnchor = htmlString.replace(new RegExp(' { + document.querySelector('.board-sidebar.sidebar').remove(); + }; + + const addJsonExportToZip = async (zip, boardSlug) => { + const downloadJSONLink = document.querySelector('.download-json-link'); + const downloadJSONURL = downloadJSONLink.href; + const response = await fetch(downloadJSONURL); + const responseBody = await response.text(); + zip.file(`data/${boardSlug}.json`, responseBody); + }; + + const closeSidebar = () => { + document.querySelector('.board-header-btn.js-toggle-sidebar').click(); + }; + + const cleanBoardHtml = () => { + Array.from(document.querySelectorAll('script')).forEach(elem => + elem.remove(), + ); + Array.from( + document.querySelectorAll('link:not([rel="stylesheet"])'), + ).forEach(elem => elem.remove()); + document.querySelector('#header-quick-access').remove(); + Array.from( + document.querySelectorAll('#header-main-bar .board-header-btns'), + ).forEach(elem => elem.remove()); + Array.from(document.querySelectorAll('.list-composer')).forEach(elem => + elem.remove(), + ); + Array.from( + document.querySelectorAll( + '.list-composer,.js-card-composer, .js-add-card', + ), + ).forEach(elem => elem.remove()); + Array.from( + document.querySelectorAll('.js-perfect-scrollbar > div:nth-of-type(n+2)'), + ).forEach(elem => elem.remove()); + Array.from(document.querySelectorAll('.js-perfect-scrollbar')).forEach( + elem => { + elem.style = 'overflow-y: auto !important;'; + elem.classList.remove('js-perfect-scrollbar'); + }, + ); + Array.from(document.querySelectorAll('[href]:not(link)')).forEach(elem => + elem.attributes.removeNamedItem('href'), + ); + Array.from(document.querySelectorAll('[href]')).forEach(elem => { + // eslint-disable-next-line no-self-assign + elem.href = elem.href; + // eslint-disable-next-line no-self-assign + elem.src = elem.src; + }); + Array.from(document.querySelectorAll('.is-editable')).forEach(elem => { + elem.classList.remove('is-editable') + }) + + }; + + const getBoardSlug = () => { + return window.location.href.split('/').pop(); + }; + + const getStylesheetList = () => { + return Array.from( + document.querySelectorAll('link[href][rel="stylesheet"]'), + ); + }; + + const downloadStylesheets = async (stylesheets, zip) => { + await asyncForEach(stylesheets, async elem => { + const response = await fetch(elem.href); + const responseBody = await response.text(); + + const finalResponse = responseBody.replace( + new RegExp('packages\/[^\/]+\/upstream\/', 'gim'), '../' + ); + + const filename = elem.href + .split('/') + .pop() + .split('?') + .shift(); + const fileFullPath = `style/${filename}`; + zip.file(fileFullPath, finalResponse); + elem.href = `../${fileFullPath}`; + }); + }; + + const getSrcAttached = () => { + return Array.from(document.querySelectorAll('[src]')); + }; + + const downloadSrcAttached = async (elements, zip, boardSlug) => { + await asyncForEach(elements, async elem => { + const response = await fetch(elem.src); + const responseBody = await response.blob(); + const filename = elem.src + .split('/') + .pop() + .split('?') + .shift(); + const fileFullPath = `${boardSlug}/${elem.tagName.toLowerCase()}/${filename}`; + zip.file(fileFullPath, responseBody); + elem.src = `./${elem.tagName.toLowerCase()}/${filename}`; + }); + }; + + const removeCssUrlSurround = url => { + const working = url || ""; + return working + .split("url(") + .join("") + .split("\")") + .join("") + .split("\"") + .join("") + .split("')") + .join("") + .split("'") + .join("") + .split(")") + .join(""); + }; + + const getCardCovers = () => { + return Array.from(document.querySelectorAll('.minicard-cover')) + .filter(elem => elem.style['background-image']) + } + + const downloadCardCovers = async (elements, zip, boardSlug) => { + await asyncForEach(elements, async elem => { + const response = await fetch(removeCssUrlSurround(elem.style['background-image'])); + const responseBody = await response.blob(); + const filename = removeCssUrlSurround(elem.style['background-image']) + .split('/') + .pop() + .split('?') + .shift() + .split('#') + .shift(); + const fileFullPath = `${boardSlug}/covers/${filename}`; + zip.file(fileFullPath, responseBody); + elem.style = "background-image: url('" + `covers/${filename}` + "')"; + }); + }; + + const addBoardHTMLToZip = (boardSlug, zip) => { + ensureSidebarRemoved(); + const htmlOutputPath = `${boardSlug}/index.html`; + zip.file(htmlOutputPath, new Blob([ + removeAnchors(getPageHtmlString()) + ], { type: 'application/html' })); + }; + + return async () => { + const zip = new JSZip(); + const boardSlug = getBoardSlug(); + + await addJsonExportToZip(zip, boardSlug); + Popup.close(); + closeSidebar(); + cleanBoardHtml(); + + await downloadStylesheets(getStylesheetList(), zip); + await downloadSrcAttached(getSrcAttached(), zip, boardSlug); + await downloadCardCovers(getCardCovers(), zip, boardSlug); + + addBoardHTMLToZip(boardSlug, zip); + + const content = await zip.generateAsync({ type: 'blob' }); + saveAs(content, `${boardSlug}.zip`); + window.location.reload(); + } +}; diff --git a/package-lock.json b/package-lock.json index 9d156628..1e69d824 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2039,6 +2039,11 @@ "minimatch": "^3.0.4" } }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -2460,6 +2465,17 @@ "minimist": "^1.2.5" } }, + "jszip": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.4.0.tgz", + "integrity": "sha512-gZAOYuPl4EhPTXT0GjhI3o+ZAz3su6EhLrKUoAivcKqyqC7laS5JEv4XWZND9BgcDcF83vI85yGbDmDR6UhrIg==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2514,6 +2530,14 @@ "type-check": "~0.3.2" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lint-staged": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz", @@ -3869,6 +3893,11 @@ "path-to-regexp": "~1.2.1" } }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "papaparse": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.2.0.tgz", @@ -4330,6 +4359,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/package.json b/package.json index 1b4d3e8a..b049a12d 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "es6-promise": "^4.2.4", "flatted": "^2.0.1", "gridfs-stream": "^0.5.3", + "jszip": "^3.4.0", "ldapjs": "^1.0.2", "meteor-node-stubs": "^0.4.1", "mongodb": "^3.5.7", -- cgit v1.2.3-1-g7c22