Skip to content

Commit

Permalink
feat: add export button
Browse files Browse the repository at this point in the history
Introduces a new `ZipWorkspace` service to allow the exporting of the current workspace as a zip.

***Example***

```javascript
// Define your workspace
const workspace = [
  {
    name: 'main.scss',
    content: '@import "variables"; .body { color: $primaryColor; }',
    type: 'scss'
  },
  {
    name: 'variables.scss',
    content: '$primaryColor: blue;',
    type: 'scss'
  }
];

// Instantiate the zip workspace and trigger the download
await new ZipWorkspace(testWorkspace).download('testWorkspace.zip');
```
  • Loading branch information
jboix committed Apr 8, 2024
1 parent 7fefb64 commit 91acbcb
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 128 deletions.
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ <h2>Craft your theme</h2>
<toggle-pane-button title="Select a file to edit" id="navigation-button">
<tree-view id="navigation"></tree-view>
</toggle-pane-button>
<button id="download">
Export as .zip
</button>
</div>
<css-editor id="editor"></css-editor>
</section>
Expand Down
290 changes: 165 additions & 125 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@srgssr/pillarbox-web": "^1.6.0",
"jszip": "^3.10.1",
"lit": "^3.1.2",
"monaco-editor": "^0.47.0",
"sass": "^1.74.1"
Expand Down
19 changes: 19 additions & 0 deletions scss/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
text-rendering: optimizelegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

--button-background: #40729d;
--button-hover-background: #305273;
--button-color: #fff;
--button-border-radius: 0.5em;
}

html,
Expand Down Expand Up @@ -99,3 +104,17 @@ footer {
width: 100%;
height: 100%;
}

#download {
padding: 1em;
color: var(--button-foreground);
background: var(--button-background);
border: none;
border-radius: var(--button-border-radius);
cursor: pointer;
transition: background-color 0.3s ease;
}

#download:hover {
background-color: var(--button-hover-background);
}
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const navigation = document.getElementById('navigation');
const navigationButton = document.getElementById('navigation-button');
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const downloadButton = document.getElementById('download');

navigation.items = sassCompiler.workspace;
navigationButton.label = currentItem.name;
Expand All @@ -28,3 +29,5 @@ editor.addEventListener('value-changed', (event) => {
currentItem.content = event.detail.value;
preview.appliedCss = sassCompiler.compile();
});

downloadButton.addEventListener('click', () => sassCompiler.download());
12 changes: 12 additions & 0 deletions src/workspace/sass-workspace-compiler.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as sass from 'sass';
import { VirtualSassImporter } from './virtual-sass-importer.js';
import ZipWorkspace from './zip-workspace.js';

/**
* Compiles SCSS files using a virtual filesystem. The compiler utilizes a
Expand Down Expand Up @@ -64,4 +65,15 @@ export class SassWorkspaceCompiler {

return sass.compileString(this.mainScss.content, opts).css;
}

/**
* Generates the zip file containing the theme workspace and triggers a download in the browser.
*
* @param {string} [fileName='pillarbox-theme.zip'] The name for the downloaded zip file.
*
* @returns {Promise<void>} A promise that resolves when the download has been triggered.
*/
async download(fileName = 'pillarbox-theme.zip') {
await new ZipWorkspace(this.workspace).download(fileName);
}
}
75 changes: 75 additions & 0 deletions src/workspace/zip-workspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import JSZip from 'jszip';

/**
* Bundles a workspace into a zip file and triggers the download.
*
* @example
* // Define your workspace
* const workspace = [
* {
* name: 'main.scss',
* content: '@import "variables"; .body { color: $primaryColor; }',
* type: 'scss'
* },
* {
* name: 'variables.scss',
* content: '$primaryColor: blue;',
* type: 'scss'
* }
* ];
*
* // Instantiate the zip workspace and trigger the download
* await new ZipWorkspace(testWorkspace).download('testWorkspace.zip');
*/
export default class ZipWorkspace {
#zip;

/**
* Initializes a new instance of the ZipWorkspace.
*
* @param {TreeItem[]} workspace An array of TreeItem objects representing the files and directories in the workspace.
*/
constructor(workspace) {
this.#zip = new JSZip();
this.#addItems(workspace);
}

/**
* Recursively adds items to the zip archive.
*
* @param {TreeItem[]} workspace An array of TreeItem objects to add to the zip. Each item can be a file or a folder.
* @param {string} [path=''] The current path in the zip archive. Used for recursive calls to maintain folder structure.
* @private
*/
#addItems(workspace, path = '') {
workspace.forEach(item => {
const currentPath = [path, item.name].filter(Boolean).join('/');

if (item.type === 'folder') {
this.#addItems(item.children, currentPath);
} else {
this.#zip.file(currentPath, item.content);
}
});
}

/**
* Generates the zip file and triggers a download in the browser.
*
* @param {string} fileName The name for the downloaded zip file.
*
* @returns {Promise<void>} A promise that resolves when the download has been triggered.
*/
async download(fileName) {
const content = await this.#zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(content);
const a = document.createElement('a');

a.download = fileName;
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
23 changes: 20 additions & 3 deletions test/workspace/sass-workspace-compiler.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import {
SassWorkspaceCompiler
} from '../../src/workspace/sass-workspace-compiler.js';
import ZipWorkspace from '../../src/workspace/zip-workspace.js';

describe('SassWorkspaceCompiler without mocks', () => {
it('compiles SCSS to CSS with imports', () => {
Expand All @@ -18,11 +19,27 @@ describe('SassWorkspaceCompiler without mocks', () => {
type: 'scss'
}
];

const compiler = new SassWorkspaceCompiler(testWorkspace, 'main.scss');

const result = compiler.compile();

expect(result).toContain('.test{color:blue}');
});

it('triggers the download of the ZIP file with the correct filename', async() => {
const workspace = [{
name: 'main.scss',
content: '',
type: 'scss'
}];
const downloadSpy = vi.spyOn(ZipWorkspace.prototype, 'download').mockImplementation(() => {});
const compiler = new SassWorkspaceCompiler(workspace, 'main.scss');

await compiler.download('custom-theme.zip');
expect(downloadSpy).toHaveBeenCalledWith('custom-theme.zip');

await compiler.download();
expect(downloadSpy).toHaveBeenCalledWith('pillarbox-theme.zip');

downloadSpy.mockRestore();
});
});
67 changes: 67 additions & 0 deletions test/workspace/zip-workspace.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from 'vitest';
import JSZip from 'jszip';
import ZipWorkspace from '../../src/workspace/zip-workspace.js';

// Mocking necessary parts of the browser API
global.URL.createObjectURL = vi.fn();
global.URL.revokeObjectURL = vi.fn();
document.body.appendChild = vi.fn();
document.body.removeChild = vi.fn();

describe('ZipWorkspace', () => {
it('creates a ZIP file with the correct structure', async() => {
// Mock JSZip instance to track method calls and verify the structure
const mockJSZipInstance = {
file: vi.fn(),
generateAsync: vi.fn().mockResolvedValue(new Blob())
};

vi.spyOn(JSZip.prototype, 'file').mockImplementation(mockJSZipInstance.file);
vi.spyOn(JSZip.prototype, 'generateAsync').mockImplementation(mockJSZipInstance.generateAsync);

const workspace = [
{
name: 'file.txt',
content: 'Hello, world!',
type: 'file'
},
{
name: 'folder',
type: 'folder',
children: [
{
name: 'nested.txt',
content: 'Nested file',
type: 'file'
}
]
}
];

const zipWorkspace = new ZipWorkspace(workspace);

await zipWorkspace.download('test.zip');

// Check if JSZip was used correctly to add files
expect(mockJSZipInstance.file).toHaveBeenCalledWith('file.txt', 'Hello, world!');
expect(mockJSZipInstance.file).toHaveBeenCalledWith('folder/nested.txt', 'Nested file');
expect(mockJSZipInstance.generateAsync).toHaveBeenCalledWith({ type: 'blob' });
});

it('triggers the download of the ZIP file', async() => {
const workspace = [{
name: 'file.txt',
content: 'Hello, world!',
type: 'file'
}];
const zipWorkspace = new ZipWorkspace(workspace);

await zipWorkspace.download('test.zip');

// Assert the download was triggered with the correct filename
expect(document.body.appendChild).toHaveBeenCalled();
expect(document.body.removeChild).toHaveBeenCalled();
expect(URL.createObjectURL).toHaveBeenCalled();
expect(URL.revokeObjectURL).toHaveBeenCalled();
});
});

0 comments on commit 91acbcb

Please sign in to comment.