diff --git a/examples/developer-tools/src/backwards_compatibility.ts b/examples/developer-tools/src/backwards_compatibility.ts index 3a3a46845a..2d2b94b41d 100644 --- a/examples/developer-tools/src/backwards_compatibility.ts +++ b/examples/developer-tools/src/backwards_compatibility.ts @@ -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; diff --git a/examples/developer-tools/src/controller.ts b/examples/developer-tools/src/controller.ts index 3c33669587..bb4bf7945f 100644 --- a/examples/developer-tools/src/controller.ts +++ b/examples/developer-tools/src/controller.ts @@ -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, @@ -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(); } @@ -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); } } @@ -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'); @@ -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); + }); + } } diff --git a/examples/developer-tools/src/index.css b/examples/developer-tools/src/index.css index 3d48bd6bc6..7997e55d00 100644 --- a/examples/developer-tools/src/index.css +++ b/examples/developer-tools/src/index.css @@ -1,3 +1,7 @@ +:root { + --md-dialog-container-color: white; +} + body { margin: 0; } @@ -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; diff --git a/examples/developer-tools/src/index.html b/examples/developer-tools/src/index.html index 864a7494d4..647f38561c 100644 --- a/examples/developer-tools/src/index.html +++ b/examples/developer-tools/src/index.html @@ -4,6 +4,9 @@
+ You can upload a file from the legacy Block Factory to import that + block here. +
++ + or drag it here +
+ +