Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
magicsunday committed Apr 26, 2024
1 parent aa21c4b commit 0101fa4
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 19 deletions.
6 changes: 3 additions & 3 deletions resources/css/svg.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,16 @@
font-size: 13px;
}

.webtrees-descendants-chart-container svg .wt-chart-box-name {
.webtrees-descendants-chart-container svg text.wt-chart-box-name {
fill: var(--link-color, currentColor);
}

.webtrees-descendants-chart-container svg .wt-chart-box-name-alt {
.webtrees-descendants-chart-container svg text.wt-chart-box-name-alt {
fill: currentColor;
font-weight: 500;
font-size: 0.85em;
}

.webtrees-descendants-chart-container svg .wt-chart-box-name:hover:not(.wt-chart-box-name-alt) {
.webtrees-descendants-chart-container svg text.wt-chart-box-name:hover:not(.wt-chart-box-name-alt) {
fill: var(--link-color-hover);
}
2 changes: 1 addition & 1 deletion resources/js/descendants-chart.min.js

Large diffs are not rendered by default.

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
5 changes: 2 additions & 3 deletions resources/js/modules/lib/tree/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ export default class Date
appendDate(parent)
{
const table = parent
.append("g")
.attr("class", "table");
.append("g");

// Top/Bottom and Bottom/Top
if ((this._orientation instanceof OrientationTopBottom)
Expand Down Expand Up @@ -112,7 +111,7 @@ export default class Date
enter
.call((g) => {
const col1 = g.append("text")
.attr("fill", "currentColor")
.attr("class", "date")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("x", d => this.textX(d))
Expand Down
5 changes: 4 additions & 1 deletion src/Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,10 @@ private function getAjaxRoute(Individual $individual, string $xref): string
*/
private function getStylesheets(): array
{
return [$this->assetUrl('css/descendants-chart.css'), $this->assetUrl('css/svg.css')];
return [
$this->assetUrl('css/descendants-chart.css'),
$this->assetUrl('css/svg.css'),
];
}

/**
Expand Down

0 comments on commit 0101fa4

Please sign in to comment.