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
70 changes: 66 additions & 4 deletions src/lib/components/Toolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import IconShare from './icons/IconShare.svelte';

let root: HTMLDivElement;
let downloadDropdownOpen = false;

export let url: string | undefined;
export let loading: boolean;
Expand All @@ -22,7 +23,8 @@
const dispatch = createEventDispatcher<{
reload: undefined;
fit: undefined;
download: undefined;
downloadPng: undefined;
downloadSvg: undefined;
copy: undefined;
share: undefined;
settings: undefined;
Expand All @@ -38,9 +40,17 @@
dispatch('copy');
};

const handleClickOutside = (event: MouseEvent) => {
if (root && !root.contains(event.target as Node)) {
downloadDropdownOpen = false;
}
};

window.addEventListener('keydown', handleKeyDown);
window.addEventListener('click', handleClickOutside);
onDestroy(() => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('click', handleClickOutside);
});
</script>

Expand Down Expand Up @@ -74,9 +84,25 @@
>
<IconFit />
</ActionButton>
<ActionButton title="Download image" disabled={noData} on:click={() => dispatch('download')}>
<IconDownload />
</ActionButton>
<div class="download-container">
<ActionButton
title="Download diagram"
disabled={noData}
on:click={() => downloadDropdownOpen = !downloadDropdownOpen}
>
<IconDownload />
</ActionButton>
{#if downloadDropdownOpen}
<div class="download-dropdown" transition:fly={{ y: 5, duration: 100 }}>
<button class="dropdown-item" on:click={() => { dispatch('downloadPng'); downloadDropdownOpen = false; }}>
Download PNG
</button>
<button class="dropdown-item" on:click={() => { dispatch('downloadSvg'); downloadDropdownOpen = false; }}>
Download SVG
</button>
</div>
{/if}
</div>
<ActionButton
title={cannotCopy
? "Copying images to clipboard doesn't work in your browser"
Expand Down Expand Up @@ -164,6 +190,42 @@
align-items: center;
}

.download-container {
position: relative;
}

.download-dropdown {
position: absolute;
top: 100%;
right: 0;
background: $color-paper;
border: 1px solid $color-shade-3;
border-radius: 4px;
box-shadow: $box-shadow;
z-index: 10;
min-width: 120px;
}

.dropdown-item {
display: block;
width: 100%;
padding: 8px 12px;
border: none;
background: none;
text-align: left;
cursor: pointer;
font-size: $font-size-small;
&:hover {
background: $color-shade-1;
}
&:first-child {
border-radius: 3px 3px 0 0;
}
&:last-child {
border-radius: 0 0 3px 3px;
}
}

.loading-icon {
display: flex;
&.loading {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/components/View.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {
copyPng,
downloadPng,
downloadSvg,
drawImage,
loadPocketbaseCollections,
validateCollections,
Expand Down Expand Up @@ -245,7 +246,8 @@
noData={!data}
on:reload={refetch}
on:fit={fit}
on:download={() => downloadPng(data, $settings)}
on:downloadPng={() => downloadPng(data, $settings)}
on:downloadSvg={() => downloadSvg(data, $settings)}
on:copy={handleCopy}
on:share={handleShare}
on:settings={() => (settingsVisible = !settingsVisible)}
Expand Down
14 changes: 13 additions & 1 deletion src/lib/diagram.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { browser } from '$app/environment';
import { draw } from 'nomnoml';
import { draw, renderSvg } from 'nomnoml';
import PocketBase, { type CollectionModel } from 'pocketbase';
import type { Settings } from './settings';
import { sanitizeId, spaces, stripBackslashes } from './utils';
Expand Down Expand Up @@ -250,6 +250,18 @@ export function downloadPng(collections: CollectionModel[] | undefined, settings
link.remove();
}

export function downloadSvg(collections: CollectionModel[] | undefined, settings: Settings) {
if (!(browser && collections)) return;
const markup = generateMarkup(collections, settings);
const svg = renderSvg(markup, document);
const blob = new Blob([svg], { type: 'image/svg+xml' });
const link = document.createElement('a');
link.download = 'pb_diagram.svg';
link.href = URL.createObjectURL(blob);
link.click();
link.remove();
}

export function copyPng(collections: CollectionModel[] | undefined, settings: Settings) {
if (!(browser && collections)) return;
const hiddenCanvas = document.createElement('canvas');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('strip backslashes', () => {

describe('spaces', () => {
it('works with positive input', () => {
expect(spaces(3)).toBe(' ');
expect(spaces(3)).toBe('\u00A0\u00A0\u00A0');
});
it('works when input is 0', () => {
expect(spaces(0)).toBe('');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export function stripBackslashes(s: string) {
}

export function spaces(n: number) {
return n < 1 ? '' : ' '.repeat(n);
return n < 1 ? '' : '\u00A0'.repeat(n); // Use non-breaking spaces for better SVG compatibility
}

export function absRoundedHalfDiff(a: number, b: number) {
Expand Down