瀏覽代碼

Export Board to Zip file

* Extracts Card covers
* Labels
* Re-works some CSS & HTML
* Produces deployable assets (minus WebFonts)
Lewis Cowles 5 年之前
父節點
當前提交
5ef83ab236
共有 5 個文件被更改,包括 253 次插入1 次删除
  1. 5 1
      client/components/sidebar/sidebar.jade
  2. 7 0
      client/components/sidebar/sidebar.js
  3. 206 0
      client/lib/exportHTML.js
  4. 34 0
      package-lock.json
  5. 1 0
      package.json

+ 5 - 1
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

+ 7 - 0
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'),

+ 206 - 0
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 `<!doctype html>${
+      window.document.querySelector('html').outerHTML
+    }`;
+  };
+
+  const removeAnchors = htmlString => {
+    const replaceOpenAnchor = htmlString.replace(new RegExp('<a ', 'gim'), '<span ');
+    return replaceOpenAnchor.replace(new RegExp('<\/a', 'gim'), '</span');
+  };
+
+  const ensureSidebarRemoved = () => {
+    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();
+  }
+};

+ 34 - 0
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",

+ 1 - 0
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",