mirror of https://github.com/wekan/wekan
The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
252 lines
7.4 KiB
252 lines
7.4 KiB
const JSZip = require('jszip');
|
|
|
|
window.ExportHtml = Popup => {
|
|
const saveAs = function(blob, filename) {
|
|
const 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('.js-pop-over, .pop-over'),
|
|
).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('[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 getWebFonts = () => {
|
|
fontUrls = [];
|
|
|
|
for (let sheet of document.styleSheets) {
|
|
// Get the base URL of the stylesheet
|
|
let baseUrl = sheet.href ? new URL(sheet.href).origin : window.location.origin;
|
|
|
|
try {
|
|
for (let rule of sheet.cssRules) {
|
|
if (rule.type === CSSRule.FONT_FACE_RULE) {
|
|
let src = rule.style.getPropertyValue('src');
|
|
let urlMatch = src.match(/url\(["']?(.+?)["']?\)/);
|
|
if (urlMatch) {
|
|
let fontUrl = urlMatch[1];
|
|
|
|
// Resolve the URL relative to the stylesheet's base URL
|
|
let resolvedUrl = new URL(fontUrl, baseUrl);
|
|
fontUrls.push(resolvedUrl.href); // Using .href to get the absolute URL
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Access to stylesheet blocked:', e);
|
|
}
|
|
}
|
|
|
|
return fontUrls;
|
|
};
|
|
|
|
const downloadFonts = async(elements, zip) => {
|
|
await asyncForEach(elements, async elem => {
|
|
const response = await fetch(elem);
|
|
const responseBody = await response.blob();
|
|
const filename = elem.split('/')
|
|
.pop()
|
|
.split('?')
|
|
.shift()
|
|
.split('#')
|
|
.shift();
|
|
const fileFullPath = `webfonts/${filename}`;
|
|
zip.file(fileFullPath, responseBody);
|
|
});
|
|
}
|
|
|
|
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.back();
|
|
closeSidebar();
|
|
cleanBoardHtml();
|
|
|
|
await downloadStylesheets(getStylesheetList(), zip);
|
|
await downloadSrcAttached(getSrcAttached(), zip, boardSlug);
|
|
await downloadCardCovers(getCardCovers(), zip, boardSlug);
|
|
await downloadFonts(getWebFonts(), zip);
|
|
|
|
addBoardHTMLToZip(boardSlug, zip);
|
|
|
|
const content = await zip.generateAsync({ type: 'blob' });
|
|
saveAs(content, `${boardSlug}.zip`);
|
|
window.location.reload();
|
|
};
|
|
};
|
|
|