mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-08-28 22:10:31 -04:00
* Some UI changes made * CSS changes * Done with UI tweaks * Changing arrow key event listeners, also changing category header in category list * Fixed bug with error on updating toolbox with read only * Changes to factory.css and workspacefactory view * Bug fixes and UI changes and refactoring to have enableKeyEvents in wfactory controller * Uncommented catch * Changes to app_controller.js to use constants, other nit changes * Nit line breaks
435 lines
16 KiB
JavaScript
435 lines
16 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2016 Google Inc.
|
|
* https://developers.google.com/blockly/
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* Controls the UI elements for workspace factory, mainly the category tabs.
|
|
* Also includes downloading files because that interacts directly with the DOM.
|
|
* Depends on WorkspaceFactoryController (for adding mouse listeners). Tabs for
|
|
* each category are stored in tab map, which associates a unique ID for a
|
|
* category with a particular tab.
|
|
*
|
|
* @author Emma Dauterman (edauterman)
|
|
*/
|
|
|
|
/**
|
|
* Class for a WorkspaceFactoryView
|
|
* @constructor
|
|
*/
|
|
|
|
WorkspaceFactoryView = function() {
|
|
// For each tab, maps ID of a ListElement to the td DOM element.
|
|
this.tabMap = Object.create(null);
|
|
};
|
|
|
|
/**
|
|
* Adds a category tab to the UI, and updates tabMap accordingly.
|
|
*
|
|
* @param {!string} name The name of the category being created
|
|
* @param {!string} id ID of category being created
|
|
* @param {boolean} firstCategory true if it's the first category, false
|
|
* otherwise
|
|
* @return {!Element} DOM element created for tab
|
|
*/
|
|
WorkspaceFactoryView.prototype.addCategoryRow =
|
|
function(name, id, firstCategory) {
|
|
var table = document.getElementById('categoryTable');
|
|
// Delete help label and enable category buttons if it's the first category.
|
|
if (firstCategory) {
|
|
document.getElementById('categoryHeader').textContent = 'Your Categories:';
|
|
}
|
|
// Create tab.
|
|
var count = table.rows.length;
|
|
var row = table.insertRow(count);
|
|
var nextEntry = row.insertCell(0);
|
|
// Configure tab.
|
|
nextEntry.id = this.createCategoryIdName(name);
|
|
nextEntry.textContent = name;
|
|
// Store tab.
|
|
this.tabMap[id] = table.rows[count].cells[0];
|
|
// Return tab.
|
|
return nextEntry;
|
|
};
|
|
|
|
/**
|
|
* Deletes a category tab from the UI and updates tabMap accordingly.
|
|
*
|
|
* @param {!string} id ID of category to be deleted.
|
|
* @param {!string} name The name of the category to be deleted.
|
|
*/
|
|
WorkspaceFactoryView.prototype.deleteElementRow = function(id, index) {
|
|
// Delete tab entry.
|
|
delete this.tabMap[id];
|
|
// Delete tab row.
|
|
var table = document.getElementById('categoryTable');
|
|
var count = table.rows.length;
|
|
table.deleteRow(index);
|
|
|
|
// If last category removed, add category help text and disable category
|
|
// buttons.
|
|
this.addEmptyCategoryMessage();
|
|
};
|
|
|
|
/**
|
|
* If there are no toolbox elements created, adds a help message to show
|
|
* where categories will appear. Should be called when deleting list elements
|
|
* in case the last element is deleted.
|
|
*/
|
|
WorkspaceFactoryView.prototype.addEmptyCategoryMessage = function() {
|
|
var table = document.getElementById('categoryTable');
|
|
if (table.rows.length == 0) {
|
|
document.getElementById('categoryHeader').textContent =
|
|
'Your categories will appear here';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given the index of the currently selected element, updates the state of
|
|
* the buttons that allow the user to edit the list elements. Updates the edit
|
|
* and arrow buttons. Should be called when adding or removing elements
|
|
* or when changing to a new element or when swapping to a different element.
|
|
*
|
|
* TODO(evd2014): Switch to using CSS to add/remove styles.
|
|
*
|
|
* @param {int} selectedIndex The index of the currently selected category,
|
|
* -1 if no categories created.
|
|
* @param {ListElement} selected The selected ListElement.
|
|
*/
|
|
WorkspaceFactoryView.prototype.updateState = function(selectedIndex, selected) {
|
|
// Disable/enable editing buttons as necessary.
|
|
document.getElementById('button_editCategory').disabled = selectedIndex < 0 ||
|
|
selected.type != ListElement.TYPE_CATEGORY;
|
|
document.getElementById('button_remove').disabled = selectedIndex < 0;
|
|
document.getElementById('button_up').disabled =
|
|
selectedIndex <= 0 ? true : false;
|
|
var table = document.getElementById('categoryTable');
|
|
document.getElementById('button_down').disabled = selectedIndex >=
|
|
table.rows.length - 1 || selectedIndex < 0 ? true : false;
|
|
// Disable/enable the workspace as necessary.
|
|
this.disableWorkspace(this.shouldDisableWorkspace(selected));
|
|
};
|
|
|
|
/**
|
|
* Determines the DOM id for a category given its name.
|
|
*
|
|
* @param {!string} name Name of category
|
|
* @return {!string} ID of category tab
|
|
*/
|
|
WorkspaceFactoryView.prototype.createCategoryIdName = function(name) {
|
|
return 'tab_' + name;
|
|
};
|
|
|
|
/**
|
|
* Switches a tab on or off.
|
|
*
|
|
* @param {!string} id ID of the tab to switch on or off.
|
|
* @param {boolean} selected True if tab should be on, false if tab should be
|
|
* off.
|
|
*/
|
|
WorkspaceFactoryView.prototype.setCategoryTabSelection =
|
|
function(id, selected) {
|
|
if (!this.tabMap[id]) {
|
|
return; // Exit if tab does not exist.
|
|
}
|
|
this.tabMap[id].className = selected ? 'tabon' : 'taboff';
|
|
};
|
|
|
|
/**
|
|
* Used to bind a click to a certain DOM element (used for category tabs).
|
|
* Taken directly from code.js
|
|
*
|
|
* @param {string|!Element} e1 tab element or corresponding id string
|
|
* @param {!Function} func Function to be executed on click
|
|
*/
|
|
WorkspaceFactoryView.prototype.bindClick = function(el, func) {
|
|
if (typeof el == 'string') {
|
|
el = document.getElementById(el);
|
|
}
|
|
el.addEventListener('click', func, true);
|
|
el.addEventListener('touchend', func, true);
|
|
};
|
|
|
|
/**
|
|
* Creates a file and downloads it. In some browsers downloads, and in other
|
|
* browsers, opens new tab with contents.
|
|
*
|
|
* @param {!string} filename Name of file
|
|
* @param {!Blob} data Blob containing contents to download
|
|
*/
|
|
WorkspaceFactoryView.prototype.createAndDownloadFile =
|
|
function(filename, data) {
|
|
var clickEvent = new MouseEvent("click", {
|
|
"view": window,
|
|
"bubbles": true,
|
|
"cancelable": false
|
|
});
|
|
var a = document.createElement('a');
|
|
a.href = window.URL.createObjectURL(data);
|
|
a.download = filename;
|
|
a.textContent = 'Download file!';
|
|
a.dispatchEvent(clickEvent);
|
|
};
|
|
|
|
/**
|
|
* Given the ID of a certain category, updates the corresponding tab in
|
|
* the DOM to show a new name.
|
|
*
|
|
* @param {!string} newName Name of string to be displayed on tab
|
|
* @param {!string} id ID of category to be updated
|
|
*
|
|
*/
|
|
WorkspaceFactoryView.prototype.updateCategoryName = function(newName, id) {
|
|
this.tabMap[id].textContent = newName;
|
|
this.tabMap[id].id = this.createCategoryIdName(newName);
|
|
};
|
|
|
|
/**
|
|
* Moves a tab from one index to another. Adjusts index inserting before
|
|
* based on if inserting before or after. Checks that the indexes are in
|
|
* bounds, throws error if not.
|
|
*
|
|
* @param {!string} id The ID of the category to move.
|
|
* @param {int} newIndex The index to move the category to.
|
|
* @param {int} oldIndex The index the category is currently at.
|
|
*/
|
|
WorkspaceFactoryView.prototype.moveTabToIndex =
|
|
function(id, newIndex, oldIndex) {
|
|
var table = document.getElementById('categoryTable');
|
|
// Check that indexes are in bounds
|
|
if (newIndex < 0 || newIndex >= table.rows.length || oldIndex < 0 ||
|
|
oldIndex >= table.rows.length) {
|
|
throw new Error('Index out of bounds when moving tab in the view.');
|
|
}
|
|
|
|
if (newIndex < oldIndex) {
|
|
// Inserting before.
|
|
var row = table.insertRow(newIndex);
|
|
row.appendChild(this.tabMap[id]);
|
|
table.deleteRow(oldIndex + 1);
|
|
} else {
|
|
// Inserting after.
|
|
var row = table.insertRow(newIndex + 1);
|
|
row.appendChild(this.tabMap[id]);
|
|
table.deleteRow(oldIndex);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Given a category ID and color, use that color to color the left border of the
|
|
* tab for that category.
|
|
*
|
|
* @param {!string} id The ID of the category to color.
|
|
* @param {!string} color The color for to be used for the border of the tab.
|
|
* Must be a valid CSS string.
|
|
*/
|
|
WorkspaceFactoryView.prototype.setBorderColor = function(id, color) {
|
|
var tab = this.tabMap[id];
|
|
tab.style.borderLeftWidth = "8px";
|
|
tab.style.borderLeftStyle = "solid";
|
|
tab.style.borderColor = color;
|
|
};
|
|
|
|
/**
|
|
* Given a separator ID, creates a corresponding tab in the view, updates
|
|
* tab map, and returns the tab.
|
|
*
|
|
* @param {!string} id The ID of the separator.
|
|
* @param {!Element} The td DOM element representing the separator.
|
|
*/
|
|
WorkspaceFactoryView.prototype.addSeparatorTab = function(id) {
|
|
// Create separator.
|
|
var table = document.getElementById('categoryTable');
|
|
var count = table.rows.length;
|
|
var row = table.insertRow(count);
|
|
var nextEntry = row.insertCell(0);
|
|
// Configure separator.
|
|
nextEntry.style.height = '10px';
|
|
// Store and return separator.
|
|
this.tabMap[id] = table.rows[count].cells[0];
|
|
return nextEntry;
|
|
};
|
|
|
|
/**
|
|
* Disables or enables the workspace by putting a div over or under the
|
|
* toolbox workspace, depending on the value of disable. Used when switching
|
|
* to/from separators where the user shouldn't be able to drag blocks into
|
|
* the workspace.
|
|
*
|
|
* @param {boolean} disable True if the workspace should be disabled, false
|
|
* if it should be enabled.
|
|
*/
|
|
WorkspaceFactoryView.prototype.disableWorkspace = function(disable) {
|
|
document.getElementById('disable_div').style.zIndex = disable ? 1 : -1;
|
|
};
|
|
|
|
/**
|
|
* Determines if the workspace should be disabled. The workspace should be
|
|
* disabled if category is a separator or has VARIABLE or PROCEDURE tags.
|
|
*
|
|
* @return {boolean} True if the workspace should be disabled, false otherwise.
|
|
*/
|
|
WorkspaceFactoryView.prototype.shouldDisableWorkspace = function(category) {
|
|
return category != null && category.type != ListElement.TYPE_FLYOUT &&
|
|
(category.type == ListElement.TYPE_SEPARATOR ||
|
|
category.custom == 'VARIABLE' || category.custom == 'PROCEDURE');
|
|
};
|
|
|
|
/*
|
|
* Removes all categories and separators in the view. Clears the tabMap to
|
|
* reflect this.
|
|
*/
|
|
WorkspaceFactoryView.prototype.clearToolboxTabs = function() {
|
|
this.tabMap = [];
|
|
var oldCategoryTable = document.getElementById('categoryTable');
|
|
var newCategoryTable = document.createElement('table');
|
|
newCategoryTable.id = 'categoryTable';
|
|
newCategoryTable.style.width = 'auto';
|
|
oldCategoryTable.parentElement.replaceChild(newCategoryTable,
|
|
oldCategoryTable);
|
|
};
|
|
|
|
/**
|
|
* Given a set of blocks currently loaded user-generated shadow blocks, visually
|
|
* marks them without making them actual shadow blocks (allowing them to still
|
|
* be editable and movable).
|
|
*
|
|
* @param {!<Blockly.Block>} blocks Array of user-generated shadow blocks
|
|
* currently loaded.
|
|
*/
|
|
WorkspaceFactoryView.prototype.markShadowBlocks = function(blocks) {
|
|
for (var i = 0; i < blocks.length; i++) {
|
|
this.markShadowBlock(blocks[i]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Visually marks a user-generated shadow block as a shadow block in the
|
|
* workspace without making the block an actual shadow block (allowing it
|
|
* to be moved and edited).
|
|
*
|
|
* @param {!Blockly.Block} block The block that should be marked as a shadow
|
|
* block (must be rendered).
|
|
*/
|
|
WorkspaceFactoryView.prototype.markShadowBlock = function(block) {
|
|
// Add Blockly CSS for user-generated shadow blocks.
|
|
Blockly.addClass_(block.svgGroup_, 'shadowBlock');
|
|
// If not a valid shadow block, add a warning message.
|
|
if (!block.getSurroundParent()) {
|
|
block.setWarningText('Shadow blocks must be nested inside' +
|
|
' other blocks to be displayed.');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Removes visual marking for a shadow block given a rendered block.
|
|
*
|
|
* @param {!Blockly.Block} block The block that should be unmarked as a shadow
|
|
* block (must be rendered).
|
|
*/
|
|
WorkspaceFactoryView.prototype.unmarkShadowBlock = function(block) {
|
|
// Remove Blockly CSS for user-generated shadow blocks.
|
|
if (Blockly.hasClass_(block.svgGroup_, 'shadowBlock')) {
|
|
Blockly.removeClass_(block.svgGroup_, 'shadowBlock');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets the tabs for modes according to which mode the user is currenly
|
|
* editing in.
|
|
*
|
|
* @param {!string} mode The mode being switched to
|
|
* (WorkspaceFactoryController.MODE_TOOLBOX or WorkspaceFactoryController.MODE_PRELOAD).
|
|
*/
|
|
WorkspaceFactoryView.prototype.setModeSelection = function(mode) {
|
|
document.getElementById('tab_preload').className = mode ==
|
|
WorkspaceFactoryController.MODE_PRELOAD ? 'tabon' : 'taboff';
|
|
document.getElementById('preload_div').style.display = mode ==
|
|
WorkspaceFactoryController.MODE_PRELOAD ? 'block' : 'none';
|
|
document.getElementById('tab_toolbox').className = mode ==
|
|
WorkspaceFactoryController.MODE_TOOLBOX ? 'tabon' : 'taboff';
|
|
document.getElementById('toolbox_div').style.display = mode ==
|
|
WorkspaceFactoryController.MODE_TOOLBOX ? 'block' : 'none';
|
|
};
|
|
|
|
/**
|
|
* Updates the help text above the workspace depending on the selected mode.
|
|
*
|
|
* @param {!string} mode The selected mode (WorkspaceFactoryController.MODE_TOOLBOX or
|
|
* WorkspaceFactoryController.MODE_PRELOAD).
|
|
*/
|
|
WorkspaceFactoryView.prototype.updateHelpText = function(mode) {
|
|
if (mode == WorkspaceFactoryController.MODE_TOOLBOX) {
|
|
var helpText = 'Drag blocks into the workspace to configure the toolbox '
|
|
+ 'in your custom workspace.';
|
|
} else {
|
|
var helpText = 'Drag blocks into the workspace to pre-load them in your '
|
|
+ 'custom workspace.'
|
|
}
|
|
document.getElementById('editHelpText').textContent = helpText;
|
|
};
|
|
|
|
/**
|
|
* Sets the basic options that are not dependent on if there are categories
|
|
* or a single flyout of blocks. Updates checkboxes and text fields.
|
|
*/
|
|
WorkspaceFactoryView.prototype.setBaseOptions = function() {
|
|
// Set basic options.
|
|
document.getElementById('option_css_checkbox').checked = true;
|
|
document.getElementById('option_maxBlocks_text').value = Infinity;
|
|
document.getElementById('option_media_text').value =
|
|
'https://blockly-demo.appspot.com/static/media/';
|
|
document.getElementById('option_readOnly_checkbox').checked = false;
|
|
document.getElementById('option_rtl_checkbox').checked = false;
|
|
document.getElementById('option_sounds_checkbox').checked = true;
|
|
|
|
// Uncheck grid and zoom options and hide suboptions.
|
|
document.getElementById('option_grid_checkbox').checked = false;
|
|
document.getElementById('grid_options').style.display = 'none';
|
|
document.getElementById('option_zoom_checkbox').checked = false;
|
|
document.getElementById('zoom_options').style.display = 'none';
|
|
|
|
// Set grid options.
|
|
document.getElementById('gridOption_spacing_text').value = 0;
|
|
document.getElementById('gridOption_length_text').value = 1;
|
|
document.getElementById('gridOption_colour_text').value = '#888';
|
|
document.getElementById('gridOption_snap_checkbox').checked = false;
|
|
|
|
// Set zoom options.
|
|
document.getElementById('zoomOption_controls_checkbox').checked = false;
|
|
document.getElementById('zoomOption_wheel_checkbox').checked = false;
|
|
document.getElementById('zoomOption_startScale_text').value = 1.0;
|
|
document.getElementById('zoomOption_maxScale_text').value = 3;
|
|
document.getElementById('zoomOption_minScale_text').value = 0.3;
|
|
document.getElementById('zoomOption_scaleSpeed_text').value = 1.2;
|
|
};
|
|
|
|
/**
|
|
* Updates category specific options depending on if there are categories
|
|
* currently present. Updates checkboxes and text fields in the view.
|
|
*
|
|
* @param {boolean} hasCategories True if categories are present, false if all
|
|
* blocks are displayed in a single flyout.
|
|
*/
|
|
WorkspaceFactoryView.prototype.setCategoryOptions = function(hasCategories) {
|
|
document.getElementById('option_collapse_checkbox').checked = hasCategories;
|
|
document.getElementById('option_comments_checkbox').checked = hasCategories;
|
|
document.getElementById('option_disable_checkbox').checked = hasCategories;
|
|
document.getElementById('option_scrollbars_checkbox').checked = hasCategories;
|
|
document.getElementById('option_trashcan_checkbox').checked = hasCategories;
|
|
}
|