Skip to content

Commit

Permalink
GH-101: WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
magicsunday committed Aug 26, 2024
1 parent eb620d4 commit 4ab355a
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 14 deletions.
4 changes: 2 additions & 2 deletions resources/js/fan-chart-2.7.1-dev.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion resources/js/modules/custom/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export default class Chart
}
)
.append("g")
.attr("class", (datum) => "person depth-" + datum.depth)
.attr("class", "person")
.attr("id", (datum) => "person-" + datum.id);

// Create a new selection in order to leave the previous enter() selection
Expand Down
135 changes: 124 additions & 11 deletions resources/js/modules/lib/chart/svg/export/svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,88 @@ import Export from "../export";
*/
export default class SvgExport extends Export
{
/**
* Replaces all CSS variables in the given CSS string with its computed style equivalent.
*
* @param {String} css
*
* @returns {String}
*/
replaceCssVariables(css)
{
// Match all CSS selectors and their content
const regexSelector = new RegExp("\\s*([^,}\\\/\\s].*)(?<!\\s).*{([\\s\\S]*?)}", "g");

// Match all properties containing an CSS variable
const regexVariables = new RegExp("\\s*([a-zA-Z0-9-_]+)??[\\s:=]*\\s*(\\bvar[(]-{2}[^)].+[)]+);", "g");

let matchesSelector;
let replacedCss = css;

// Match all CSS selectors and their content
while ((matchesSelector = regexSelector.exec(css)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (matchesSelector.index === regexSelector.lastIndex) {
regexSelector.lastIndex++;
}

// Use the selector to look up the element in the DOM
const element = document.querySelector(matchesSelector[1].trim());

let matchesVariables;

// Match all properties of the previous matched selector and check if it contains an CSS variable
while ((matchesVariables = regexVariables.exec(matchesSelector[2])) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (matchesVariables.index === regexVariables.lastIndex) {
regexVariables.lastIndex++;
}

// If the element was not found, remove the CSS variable and its property
if (element === null) {
replacedCss = replacedCss.replace(matchesVariables[0], "");
continue;
}

// Get the computed style of the property
const computedFillProperty = window
.getComputedStyle(element)
.getPropertyValue(matchesVariables[1]);

// Replace the variable property with the computed style
if (computedFillProperty !== "") {
replacedCss = replacedCss.replace(matchesVariables[2], computedFillProperty);
}
}
}

return replacedCss;
}

/**
* Returns an unique sorted list of class names from all SVG elements.
*
* @param {NodeListOf<Element>} elements
*
* @returns {String[]}
*/
extractClassNames(elements)
{
let classes = {};

return Array.prototype
.concat
.apply(
[],
[...elements].map(function (element) {
return [...element.classList];
})
)
// Reduce the list of classNames to a unique list
.filter(name => !classes[name] && (classes[name] = true))
.sort();
}

/**
* Copies recursively all the styles from the list of container elements from the source
* to the destination node.
Expand All @@ -29,22 +111,53 @@ export default class SvgExport extends Export
*/
copyStylesInline(cssFiles, destinationNode, containerClassName)
{
// Assign class wt-global so theme related styles are correctly set in export
destinationNode.classList.add("wt-global");

const elementsWithClass = destinationNode.querySelectorAll("[class]");
const usedClasses = this.extractClassNames(elementsWithClass);
usedClasses.push("wt-global", containerClassName);

const style = document.createElementNS(
"http://www.w3.org/2000/svg",
"style"
);

let cssMap = new Map();

return new Promise(resolve => {
Promise
.all(cssFiles.map(url => d3.text(url)))
.then((filesData) => {
filesData.forEach(data => {
// Remove parent container selector as the CSS is included directly in the SVG element
data = data.replace(new RegExp("." + containerClassName + " ", "g"), "");
const classList = "\\." + usedClasses.join("|\\.");
const regex = new RegExp("(([^,}]*)(" + classList + "))\\b(?!-)[^}]*}", 'g')

let matches;

let style = document.createElementNS("http://www.w3.org/2000/svg", "style");
style.appendChild(document.createTextNode(data));
while ((matches = regex.exec(data)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (matches.index === regex.lastIndex) {
regex.lastIndex++;
}

destinationNode.prepend(style);
// Store all matches CSS rules (merge duplicates into same entry)
cssMap.set(
JSON.stringify(matches[0]),
matches[0]
);
}
});

// Assign class wt-global so theme related styles are correctly set in export
destinationNode.classList.add("wt-global");
// Convert the CSS map to the final CSS string
let finalCss = [...cssMap.values()].flat().join("\n");

// Remove parent container selector as the CSS is included directly in the SVG element
finalCss = this.replaceCssVariables(finalCss);
finalCss = finalCss.replaceAll("." + containerClassName + " ", "");

style.appendChild(document.createTextNode(finalCss));
destinationNode.prepend(style);

resolve(destinationNode);
});
Expand All @@ -61,11 +174,11 @@ export default class SvgExport extends Export
convertToObjectUrl(svg)
{
return new Promise(resolve => {
let data = (new XMLSerializer()).serializeToString(svg);
let DOMURL = window.URL || window.webkitURL || window;
let data = (new XMLSerializer()).serializeToString(svg);
let DOMURL = window.URL || window.webkitURL || window;
let svgBlob = new Blob([ data ], { type: "image/svg+xml;charset=utf-8" });
let url = DOMURL.createObjectURL(svgBlob);
let img = new Image();
let url = DOMURL.createObjectURL(svgBlob);
let img = new Image();

img.onload = () => {
resolve(url);
Expand Down

0 comments on commit 4ab355a

Please sign in to comment.