Skip to content

Commit

Permalink
feat: add file upload for block factory (#2320)
Browse files Browse the repository at this point in the history
* feat: add file upload for block factory

* chore: fix questionable html formatting

* chore: rename and comments
  • Loading branch information
maribethb authored Apr 11, 2024
1 parent 750aa45 commit 8a8ccdf
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 2 deletions.
4 changes: 4 additions & 0 deletions examples/developer-tools/src/backwards_compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const CONNECTION_CHECK_SHADOW = {
export function convertBaseBlock(
oldBlock: Blockly.serialization.blocks.State,
): Blockly.serialization.blocks.State {
if (oldBlock.type !== 'factory_base') {
throw Error('Malformed block data');
}

const newBlock = {...oldBlock};
// extraState from the old tool isn't relevant.
delete newBlock.extraState;
Expand Down
109 changes: 107 additions & 2 deletions examples/developer-tools/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@

import * as Blockly from 'blockly';
import * as storage from './storage';
import {createNewBlock, loadBlock} from './serialization';
import {createNewBlock, loadBlock, loadBlockFromData} from './serialization';
import {ViewModel} from './view_model';
import {JavascriptDefinitionGenerator} from './output-generators/javascript_definition_generator';
import {JsonDefinitionGenerator} from './output-generators/json_definition_generator';
import {CodeHeaderGenerator} from './output-generators/code_header_generator';
import {Menu} from '@material/web/menu/menu';
import {GeneratorStubGenerator} from './output-generators/generator_stub_generator';
import {convertBaseBlock} from './backwards_compatibility';

const IMPORT_BLOCK_FACTORY_ID = 'Import from block factory...';

/**
* This class handles updating the UI output, including refreshing the block preview,
Expand Down Expand Up @@ -51,6 +54,33 @@ export class Controller {
this.handleLoadButton();
});

this.viewModel.fileModalCloseButton.addEventListener('click', () => {
this.viewModel.toggleFileUploadModal(false);
});

this.viewModel.fileDropZone.addEventListener('drop', (e) => {
this.handleFileDrop(e);
});

this.viewModel.fileUploadInput.addEventListener('change', () => {
this.handleFileUpload();
});

this.viewModel.fileLabel.addEventListener('keydown', function (ev) {
// Clicks the file input if you hit enter on the label for the input
if (ev.key === 'Enter') (ev.target as HTMLElement).click();
});

this.viewModel.fileDropZone.addEventListener('dragover', (ev) => {
ev.preventDefault();
this.viewModel.fileDropZone.classList.add('isDragging');
});

this.viewModel.fileDropZone.addEventListener('dragleave', (ev) => {
ev.preventDefault();
this.viewModel.fileDropZone.classList.remove('isDragging');
});

// Load previously-saved settings once on page load
this.loadBlockFactorySettings();
}
Expand Down Expand Up @@ -210,6 +240,10 @@ export class Controller {
private handleLoadSelect(e: Event) {
if (e.target && e.target instanceof HTMLElement) {
const blockName = e.target.getAttribute('data-id');
if (blockName === IMPORT_BLOCK_FACTORY_ID) {
this.handleLoadFromFile();
return;
}
loadBlock(this.mainWorkspace, blockName);
}
}
Expand All @@ -222,7 +256,9 @@ export class Controller {
*/
private populateLoadMenu() {
const menuEl = this.viewModel.loadMenu;
const newItems = Array.from(storage.getAllSavedBlockNames()).map((name) => {
const optionNames = Array.from(storage.getAllSavedBlockNames());
optionNames.push(IMPORT_BLOCK_FACTORY_ID);
const newItems = optionNames.map((name) => {
const el = document.createElement('md-menu-item');
el.setAttribute('data-id', name);
const div = document.createElement('div');
Expand All @@ -236,4 +272,73 @@ export class Controller {
});
menuEl.replaceChildren(...newItems);
}

/** Shows the file upload modal when the user selects that option from the load menu. */
private handleLoadFromFile() {
this.viewModel.toggleFileUploadModal(true);
}

/**
* Handles the drag event that occurs when a user drops a file onto the file upload
* drag-n-drop zone. Gets the first file of the type we want and attempts to load it
* into the block factory editor.
*
* @param e drop event
*/
private handleFileDrop(e: DragEvent) {
// Prevent default behavior (Prevent file from being opened)
e.preventDefault();

this.viewModel.toggleFileUploadWarning(false);

this.viewModel.fileDropZone.classList.remove('isDragging');

if (!e.dataTransfer.items) return;

const firstItem = [...e.dataTransfer.items].find((item) => {
// Get the first plain text file, in case the user uploaded multiple
if (item.kind === 'file' && item.type === 'text/plain') {
return true;
}
});

if (!firstItem) {
this.viewModel.toggleFileUploadWarning(true);
return;
}
this.loadFromFile(firstItem.getAsFile());
}

/**
* Handles the event that occurs when a user picks a file from the file input.
* Attempts to load the file into the block factory editor.
*/
private handleFileUpload() {
this.viewModel.toggleFileUploadWarning(false);
const input = this.viewModel.fileUploadInput as HTMLInputElement;
const file = input.files[0];
if (!file) return;
this.loadFromFile(file);
}

/**
* Given a file, tries to run it through our backwards-compatibility converter
* and load the block into block factory. If there's an error at any point,
* we show a warning and allow the user to try the upload again.
*
* @param file File containing the block json to load. This should be the file
* downloaded directly from the old block factory tool.
*/
private loadFromFile(file: File) {
file
.text()
.then((contents) => {
const fixedBlockData = convertBaseBlock(JSON.parse(contents));
loadBlockFromData(this.mainWorkspace, fixedBlockData);
this.viewModel.toggleFileUploadModal(false);
})
.catch((e) => {
this.viewModel.toggleFileUploadWarning(true);
});
}
}
52 changes: 52 additions & 0 deletions examples/developer-tools/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:root {
--md-dialog-container-color: white;
}

body {
margin: 0;
}
Expand Down Expand Up @@ -83,6 +87,54 @@ md-menu {
max-height: 430px;
}

#file-upload-drop-zone {
background-color: white;
outline: 2px dashed #2bb4ca;
outline-offset: -10px;
border-radius: 8px;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

#file-upload-drop-zone > * {
flex-grow: 0;
margin: 12px 24px;
}
#file-upload-drop-zone > span {
font-size: 48px;
}

#file-upload-drop-zone.isDragging {
background-color: rgba(0, 0, 0, 0.1);
}

#file-label {
font-weight: 700;
cursor: pointer;
}

.warning-message {
visibility: hidden;
color: #b00020;
text-align: center;
}

/* Hides an element visually but not from screen readers */
.visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}

@media screen and (max-width: 900px) {
#block-factory-container {
flex-direction: column;
Expand Down
38 changes: 38 additions & 0 deletions examples/developer-tools/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Blockly Developer Tools</title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Google+Symbols:opsz,wght,FILL,GRAD@48,400,0,0" />
</head>
<body>
<div class="page-container">
Expand All @@ -22,6 +25,41 @@
<button id="delete-btn" class="btn">Delete</button>
</div>
<div id="block-factory-container">
<md-dialog id="file-upload-modal">
<div slot="headline">Import from Block Factory</div>
<div slot="content">
<p>
You can upload a file from the legacy Block Factory to import that
block here.
</p>
<div id="file-upload-drop-zone">
<span class="google-symbols" aria-hidden="true">upload_file</span>
<input
type="file"
id="file-upload"
class="visually-hidden"
accept=".txt" />
<p>
<label for="file-upload" id="file-label" tabindex="1">
Choose a file
</label>
or drag it here
</p>
<p id="file-upload-warning" class="warning-message">
File could not be parsed. Make sure you're uploading the file
you downloaded from the legacy Block Factory.
</p>
</div>
</div>
<div slot="actions">
<md-text-button
id="file-upload-close"
form="form-id"
value="cancel">
Cancel
</md-text-button>
</div>
</md-dialog>
<div id="main-workspace"></div>
<div id="output-pane" class="panel-container">
<div id="block-preview"></div>
Expand Down
24 changes: 24 additions & 0 deletions examples/developer-tools/src/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ export function loadBlock(workspace: Blockly.Workspace, blockName?: string) {
}
}

/**
* Loads given block json into the given workspace.
*
* @param workspace Blockly workspace to load into.
* @param blockJson Block json to load. This is state representing a single block,
* not the entire workspace.
*/
export function loadBlockFromData(
workspace: Blockly.Workspace,
blockJson: Blockly.serialization.blocks.State,
) {
// Disable events so we don't save while deleting blocks.
Blockly.Events.disable();
workspace.clear();
Blockly.Events.enable();

// There might be conflicts when loading a block from a file.
// If so, find a similar unused name so we don't overwrite.
const blockName = getNewUnusedName(blockJson.fields.NAME);
blockJson.fields.NAME = blockName;

Blockly.serialization.blocks.append(blockJson, workspace);
}

/**
* Creates a new block from scratch.
*
Expand Down
45 changes: 45 additions & 0 deletions examples/developer-tools/src/view_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import '@material/web/menu/menu';
import '@material/web/menu/menu-item';
import '@material/web/dialog/dialog';
import '@material/web/button/text-button';

export class ViewModel {
mainWorkspaceDiv = document.getElementById('main-workspace');
Expand All @@ -20,6 +22,13 @@ export class ViewModel {
loadButton = document.getElementById('load-btn');
loadMenu = document.getElementById('load-menu');

fileModal = document.getElementById('file-upload-modal');
fileModalCloseButton = document.getElementById('file-upload-close');
fileDropZone = document.getElementById('file-upload-drop-zone');
fileUploadInput = document.getElementById('file-upload');
fileLabel = document.getElementById('file-label');
fileUploadWarning = document.getElementById('file-upload-warning');

/**
* Gets a string representing the format of the block
* definition the user selected.
Expand Down Expand Up @@ -92,4 +101,40 @@ export class ViewModel {
? (importButton.checked = true)
: (scriptButton.checked = true);
}

/**
* Shows or hides the file upload modal.
*
* @param show true to show, false to hide.
*/
toggleFileUploadModal(show: boolean) {
if (show) {
this.fileModal.setAttribute('open', '');

// Set z-index of scrim so that it covers the Blockly toolbox
// https://github.com/material-components/material-web/issues/4948
if (this.fileModal.shadowRoot) {
const scrim =
this.fileModal.shadowRoot.querySelector<HTMLElement>('.scrim');
if (scrim) {
scrim.style.zIndex = '99';
}
}
} else {
this.fileModal.removeAttribute('open');
}
}

/**
* Shows or hides the file upload error message.
*
* @param show true to show, false to hide.
*/
toggleFileUploadWarning(show: boolean) {
if (show) {
this.fileUploadWarning.style.visibility = 'visible';
} else {
this.fileUploadWarning.style.visibility = 'hidden';
}
}
}

0 comments on commit 8a8ccdf

Please sign in to comment.