Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions web_timeline/static/src/views/timeline/timeline_renderer.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,62 @@ import {useService} from "@web/core/utils/hooks";

const {DateTime} = luxon;

/**
* Inline computed styles from a source DOM tree into a cloned tree.
*
* This is required because the exported SVG must be self-contained:
* external CSS from Odoo assets is not available once the SVG is opened
* outside the browser.
*
*/

function inlineAllStyles(srcRoot, dstRoot) {
const srcEls = [srcRoot, ...srcRoot.querySelectorAll("*")];
const dstEls = [dstRoot, ...dstRoot.querySelectorAll("*")];

const props = [
"display",
"position",
"top",
"left",
"right",
"bottom",
"width",
"height",
"margin",
"padding",
"border",
"border-radius",
"background",
"background-color",
"color",
"font",
"font-size",
"font-family",
"font-weight",
"line-height",
"text-align",
"white-space",
"transform",
"box-sizing",
];

for (let i = 0; i < srcEls.length; i++) {
const src = srcEls[i];
const dst = dstEls[i];
if (!dst) continue;

const cs = window.getComputedStyle(src);
const style = props.map((p) => `${p}:${cs.getPropertyValue(p)};`).join("");
dst.setAttribute("style", style);
}
}

export class TimelineRenderer extends Component {
setup() {
super.setup?.();
this.timelineRef = useRef("timelineRoot");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timelineRef variable is unused, so it is not necessary; please remove it.


this.orm = useService("orm");
this.rootRef = useRef("root");
this.canvasRef = useRef("canvas");
Expand Down Expand Up @@ -144,6 +198,51 @@ export class TimelineRenderer extends Component {
}
}

/**
* Export the currently visible timeline as an SVG file.
*
* The vis-timeline library renders the timeline using HTML elements
* instead of SVG. To allow exporting, the rendered DOM is embedded
* inside an SVG using a <foreignObject>.
*
* Computed styles are inlined so the exported file preserves the
* visual appearance outside Odoo.
*
* NOTE:
* - Uses SVG foreignObject (viewer compatibility may vary).
*/

exportSVG() {
const root = this.rootRef.el;
if (!root) return;

const timelineEl = root.querySelector(".vis-timeline");
if (!timelineEl) return;

const rect = timelineEl.getBoundingClientRect();

const cloned = timelineEl.cloneNode(true);

inlineAllStyles(timelineEl, cloned);

const svg = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${Math.ceil(rect.width)}" height="${Math.ceil(rect.height)}">
<foreignObject width="100%" height="100%">
<div xmlns="http://www.w3.org/1999/xhtml">
${cloned.outerHTML}
</div>
</foreignObject>
</svg>`;

const blob = new Blob([svg], {type: "image/svg+xml;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "timeline.svg";
a.click();
URL.revokeObjectURL(url);
}

/**
* Computes the initial visible window.
*
Expand Down
45 changes: 25 additions & 20 deletions web_timeline/static/src/views/timeline/timeline_renderer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@
<t t-name="web_timeline.TimelineRenderer">
<div class="oe_timeline_view" t-ref="root">
<div class="oe_timeline_buttons">
<button
t-att-class="'oe_timeline_button_today btn ' + (mode.data == 'today' ? ' btn-primary' : 'btn-default')"
t-on-click="_onTodayClicked"
>Today</button>
<div class="btn-group btn-sm">
<div class="btns-group">
<button
t-att-class="'oe_timeline_button_scale_day btn ' + (mode.data == 'day' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleDayClicked"
>Day</button>
<button
t-att-class="'oe_timeline_button_scale_week btn ' + (mode.data == 'week' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleWeekClicked"
>Week</button>
<button
t-att-class="'oe_timeline_button_scale_month btn ' + (mode.data == 'month' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleMonthClicked"
>Month</button>
<button
t-att-class="'oe_timeline_button_scale_year btn ' + (mode.data == 'year' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleYearClicked"
>Year</button>
t-att-class="'oe_timeline_button_today btn ' + (mode.data == 'today' ? ' btn-primary' : 'btn-default')"
t-on-click="_onTodayClicked"
>Today</button>
<div class="btn-group btn-sm">
<button
t-att-class="'oe_timeline_button_scale_day btn ' + (mode.data == 'day' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleDayClicked"
>Day</button>
<button
t-att-class="'oe_timeline_button_scale_week btn ' + (mode.data == 'week' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleWeekClicked"
>Week</button>
<button
t-att-class="'oe_timeline_button_scale_month btn ' + (mode.data == 'month' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleMonthClicked"
>Month</button>
<button
t-att-class="'oe_timeline_button_scale_year btn ' + (mode.data == 'year' ? ' btn-primary' : 'btn-default')"
t-on-click="_onScaleYearClicked"
>Year</button>
</div>
</div>
<button t-on-click="exportSVG" type='button' class="btn btn-default">
Export SVG
</button>
</div>
<div class="oe_timeline_widget" t-ref="canvas" />
</div>
Expand Down
5 changes: 5 additions & 0 deletions web_timeline/static/src/views/timeline/timeline_view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ $vis-item-content-padding: 0 3px !important;
}
}
}

.oe_timeline_buttons {
display: flex;
justify-content: space-between;
}