mirror of
https://github.com/google/blockly.git
synced 2026-01-20 15:27:09 +01:00
Merge pull request #545 from rachel-fenichel/feature/variable_management
Merge from develop.
This commit is contained in:
@@ -25,15 +25,16 @@ the main component to be loaded. This will usually be blocklyApp.AppView, but
|
||||
if you have another component that wraps it, use that one instead.
|
||||
|
||||
|
||||
Customizing the Toolbar
|
||||
-----------------------
|
||||
Customizing the Toolbar and Audio
|
||||
---------------------------------
|
||||
The Accessible Blockly workspace comes with a customizable toolbar.
|
||||
|
||||
To customize the toolbar, you will need to declare an ACCESSIBLE_GLOBALS object
|
||||
in the global scope that looks like this:
|
||||
|
||||
var ACCESSIBLE_GLOBALS = {
|
||||
toolbarButtonConfig: []
|
||||
toolbarButtonConfig: [],
|
||||
mediaPathPrefix: null
|
||||
};
|
||||
|
||||
The value corresponding to 'toolbarButtonConfig' can be modified by adding
|
||||
@@ -43,6 +44,9 @@ two keys:
|
||||
- 'text' (the text to display on the button)
|
||||
- 'action' (the function that gets run when the button is clicked)
|
||||
|
||||
In addition, if you want audio to be played, set mediaPathPrefix to the
|
||||
location of the accessible/media folder.
|
||||
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
@@ -64,7 +64,8 @@ blocklyApp.AppView = ng.core
|
||||
// https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/
|
||||
providers: [
|
||||
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService]
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
blocklyApp.AudioService]
|
||||
})
|
||||
.Class({
|
||||
constructor: [blocklyApp.NotificationsService, function(_notificationsService) {
|
||||
|
||||
57
accessible/audio.service.js
Normal file
57
accessible/audio.service.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* AccessibleBlockly
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Angular2 Service that plays audio files.
|
||||
* @author sll@google.com (Sean Lip)
|
||||
*/
|
||||
|
||||
blocklyApp.AudioService = ng.core
|
||||
.Class({
|
||||
constructor: [function() {
|
||||
// We do not play any audio unless a media path prefix is specified.
|
||||
this.canPlayAudio = false;
|
||||
if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) {
|
||||
this.canPlayAudio = true;
|
||||
var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix'];
|
||||
this.AUDIO_PATHS_ = {
|
||||
'connect': mediaPathPrefix + 'click.mp3',
|
||||
'delete': mediaPathPrefix + 'delete.mp3'
|
||||
};
|
||||
}
|
||||
|
||||
// TODO(sll): Add ogg and mp3 fallbacks.
|
||||
this.cachedAudioFiles_ = {};
|
||||
}],
|
||||
play_: function(audioId) {
|
||||
if (this.canPlayAudio) {
|
||||
if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) {
|
||||
this.cachedAudioFiles_[audioId] = new Audio(
|
||||
this.AUDIO_PATHS_[audioId]);
|
||||
}
|
||||
this.cachedAudioFiles_[audioId].play();
|
||||
}
|
||||
},
|
||||
playConnectSound: function() {
|
||||
this.play_('connect');
|
||||
},
|
||||
playDeleteSound: function() {
|
||||
this.play_('delete');
|
||||
}
|
||||
});
|
||||
@@ -26,7 +26,8 @@ blocklyApp.ClipboardService = ng.core
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.NotificationsService, blocklyApp.UtilsService,
|
||||
function(_notificationsService, _utilsService) {
|
||||
blocklyApp.AudioService,
|
||||
function(_notificationsService, _utilsService, _audioService) {
|
||||
this.clipboardBlockXml_ = null;
|
||||
this.clipboardBlockPreviousConnection_ = null;
|
||||
this.clipboardBlockNextConnection_ = null;
|
||||
@@ -34,6 +35,7 @@ blocklyApp.ClipboardService = ng.core
|
||||
this.markedConnection_ = null;
|
||||
this.notificationsService = _notificationsService;
|
||||
this.utilsService = _utilsService;
|
||||
this.audioService = _audioService;
|
||||
}],
|
||||
areConnectionsCompatible_: function(blockConnection, connection) {
|
||||
// Check that both connections exist, that it's the right kind of
|
||||
@@ -130,6 +132,7 @@ blocklyApp.ClipboardService = ng.core
|
||||
default:
|
||||
connection.connect(reconstitutedBlock.outputConnection);
|
||||
}
|
||||
this.audioService.playConnectSound();
|
||||
this.notificationsService.setStatusMessage(
|
||||
this.utilsService.getBlockDescription(reconstitutedBlock) + ' ' +
|
||||
Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG);
|
||||
@@ -151,6 +154,7 @@ blocklyApp.ClipboardService = ng.core
|
||||
if (this.areConnectionsCompatible_(
|
||||
this.markedConnection_, potentialConnections[i])) {
|
||||
this.markedConnection_.connect(potentialConnections[i]);
|
||||
this.audioService.playConnectSound();
|
||||
connectionSuccessful = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ blocklyApp.FieldComponent = ng.core
|
||||
template: `
|
||||
<input *ngIf="isTextInput()" [id]="mainFieldId" type="text" [disabled]="disabled"
|
||||
[ngModel]="field.getValue()" (ngModelChange)="field.setValue($event)"
|
||||
[attr.aria-label]="disabled ? 'Disabled text field' : 'Press Enter to edit text'">
|
||||
[attr.aria-label]="disabled ? 'Disabled text field' : 'Press Enter to edit text'"
|
||||
tabindex="-1">
|
||||
|
||||
<input *ngIf="isNumberInput()" [id]="mainFieldId" type="number" [disabled]="disabled"
|
||||
[ngModel]="field.getValue()" (ngModelChange)="field.setValue($event)"
|
||||
[attr.aria-label]="disabled ? 'Disabled number field' : 'Press Enter to edit number'">
|
||||
[attr.aria-label]="disabled ? 'Disabled number field' : 'Press Enter to edit number'"
|
||||
tabindex="-1">
|
||||
|
||||
<div *ngIf="isDropdown()"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-argument-menu', idMap['label'])">
|
||||
@@ -43,7 +45,7 @@ blocklyApp.FieldComponent = ng.core
|
||||
<li [id]="idMap[optionValue]" role="treeitem" *ngFor="#optionValue of getOptions()"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[optionValue + 'Button'], 'blockly-button')">
|
||||
<button [id]="idMap[optionValue + 'Button']" (click)="handleDropdownChange(field, optionValue)"
|
||||
[disabled]="disabled">
|
||||
[disabled]="disabled" tabindex="-1">
|
||||
{{optionText[optionValue]}}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
BIN
accessible/media/click.mp3
Normal file
BIN
accessible/media/click.mp3
Normal file
Binary file not shown.
BIN
accessible/media/click.ogg
Normal file
BIN
accessible/media/click.ogg
Normal file
Binary file not shown.
BIN
accessible/media/click.wav
Normal file
BIN
accessible/media/click.wav
Normal file
Binary file not shown.
BIN
accessible/media/delete.mp3
Normal file
BIN
accessible/media/delete.mp3
Normal file
Binary file not shown.
BIN
accessible/media/delete.ogg
Normal file
BIN
accessible/media/delete.ogg
Normal file
Binary file not shown.
BIN
accessible/media/delete.wav
Normal file
BIN
accessible/media/delete.wav
Normal file
Binary file not shown.
@@ -37,14 +37,14 @@ blocklyApp.ToolboxTreeComponent = ng.core
|
||||
<li [id]="idMap['workspaceCopy']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['workspaceCopyButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level + 2">
|
||||
<button [id]="idMap['workspaceCopyButton']" (click)="copyToWorkspace()">
|
||||
<button [id]="idMap['workspaceCopyButton']" (click)="copyToWorkspace()" tabindex="-1">
|
||||
{{'COPY_TO_WORKSPACE'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
<li [id]="idMap['blockCopy']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['blockCopyButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level + 2">
|
||||
<button [id]="idMap['blockCopyButton']" (click)="copyToClipboard()">
|
||||
<button [id]="idMap['blockCopyButton']" (click)="copyToClipboard()" tabindex="-1">
|
||||
{{'COPY_TO_CLIPBOARD'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
@@ -52,7 +52,7 @@ blocklyApp.ToolboxTreeComponent = ng.core
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['sendToSelectedButton'], 'blockly-button', !canBeCopiedToMarkedConnection())"
|
||||
[attr.aria-level]="level + 2">
|
||||
<button [id]="idMap['sendToSelectedButton']" (click)="copyToMarkedSpot()"
|
||||
[disabled]="!canBeCopiedToMarkedConnection()">
|
||||
[disabled]="!canBeCopiedToMarkedConnection()" tabindex="-1">
|
||||
{{'COPY_TO_MARKED_SPOT'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -30,7 +30,7 @@ blocklyApp.ToolboxComponent = ng.core
|
||||
<h3 #toolboxTitle id="blockly-toolbox-title">Toolbox</h3>
|
||||
<ol #tree
|
||||
id="blockly-toolbox-tree" role="tree" class="blocklyTree"
|
||||
*ngIf="toolboxCategories && toolboxCategories.length > 0" tabIndex="0"
|
||||
*ngIf="toolboxCategories && toolboxCategories.length > 0" tabindex="0"
|
||||
[attr.aria-labelledby]="toolboxTitle.id"
|
||||
[attr.aria-activedescendant]="getActiveDescId()"
|
||||
(keydown)="treeService.onKeypress($event, tree)">
|
||||
|
||||
@@ -82,21 +82,19 @@ blocklyApp.TreeService = ng.core
|
||||
}
|
||||
return null;
|
||||
},
|
||||
focusOnNextTree_: function(treeId) {
|
||||
getIdOfNextTree_: function(treeId) {
|
||||
var trees = this.getAllTreeNodes_();
|
||||
for (var i = 0; i < trees.length - 1; i++) {
|
||||
if (trees[i].id == treeId) {
|
||||
trees[i + 1].focus();
|
||||
return trees[i + 1].id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
focusOnPreviousTree_: function(treeId) {
|
||||
getIdOfPreviousTree_: function(treeId) {
|
||||
var trees = this.getAllTreeNodes_();
|
||||
for (var i = trees.length - 1; i > 0; i--) {
|
||||
if (trees[i].id == treeId) {
|
||||
trees[i - 1].focus();
|
||||
return trees[i - 1].id;
|
||||
}
|
||||
}
|
||||
@@ -190,12 +188,11 @@ blocklyApp.TreeService = ng.core
|
||||
if (e.keyCode == 9) {
|
||||
// Tab key.
|
||||
var destinationTreeId =
|
||||
e.shiftKey ? this.focusOnPreviousTree_(treeId) :
|
||||
this.focusOnNextTree_(treeId);
|
||||
this.notifyUserAboutCurrentTree_(destinationTreeId);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.shiftKey ? this.getIdOfPreviousTree_(treeId) :
|
||||
this.getIdOfNextTree_(treeId);
|
||||
if (destinationTreeId) {
|
||||
this.notifyUserAboutCurrentTree_(destinationTreeId);
|
||||
}
|
||||
}
|
||||
},
|
||||
isButtonOrFieldNode_: function(node) {
|
||||
@@ -260,16 +257,20 @@ blocklyApp.TreeService = ng.core
|
||||
// For Esc and Tab keys, the focus is removed from the input field.
|
||||
this.focusOnCurrentTree_(treeId);
|
||||
|
||||
// In addition, for Tab keys, the user tabs to the previous/next tree.
|
||||
if (e.keyCode == 9) {
|
||||
var destinationTreeId =
|
||||
e.shiftKey ? this.focusOnPreviousTree_(treeId) :
|
||||
this.focusOnNextTree_(treeId);
|
||||
this.notifyUserAboutCurrentTree_(destinationTreeId);
|
||||
e.shiftKey ? this.getIdOfPreviousTree_(treeId) :
|
||||
this.getIdOfNextTree_(treeId);
|
||||
if (destinationTreeId) {
|
||||
this.notifyUserAboutCurrentTree_(destinationTreeId);
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Allow Tab keypresses to go through.
|
||||
if (e.keyCode == 27) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Outside an input field, Enter, Tab and navigation keys are all
|
||||
@@ -302,14 +303,14 @@ blocklyApp.TreeService = ng.core
|
||||
}
|
||||
}
|
||||
} else if (e.keyCode == 9) {
|
||||
// Tab key.
|
||||
// Tab key. Note that allowing the event to propagate through is
|
||||
// intentional.
|
||||
var destinationTreeId =
|
||||
e.shiftKey ? this.focusOnPreviousTree_(treeId) :
|
||||
this.focusOnNextTree_(treeId);
|
||||
this.notifyUserAboutCurrentTree_(destinationTreeId);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.shiftKey ? this.getIdOfPreviousTree_(treeId) :
|
||||
this.getIdOfNextTree_(treeId);
|
||||
if (destinationTreeId) {
|
||||
this.notifyUserAboutCurrentTree_(destinationTreeId);
|
||||
}
|
||||
} else if (e.keyCode >= 35 && e.keyCode <= 40) {
|
||||
// End, home, and arrow keys.
|
||||
if (e.keyCode == 35) {
|
||||
|
||||
@@ -60,7 +60,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
[attr.aria-level]="level + 2">
|
||||
<button [id]="idMap[fieldButtonInfo.baseIdKey + 'Button' + i]"
|
||||
(click)="fieldButtonInfo.action(inputBlock.connection)"
|
||||
[disabled]="fieldButtonInfo.isDisabled(inputBlock.connection)">
|
||||
[disabled]="fieldButtonInfo.isDisabled(inputBlock.connection)" tabindex="-1">
|
||||
{{fieldButtonInfo.translationIdForText|translate}}
|
||||
</button>
|
||||
</li>
|
||||
@@ -78,7 +78,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button', buttonInfo.isDisabled())"
|
||||
[attr.aria-level]="level + 2">
|
||||
<button [id]="idMap[buttonInfo.baseIdKey + 'Button']" (click)="buttonInfo.action()"
|
||||
[disabled]="buttonInfo.isDisabled()">
|
||||
[disabled]="buttonInfo.isDisabled()" tabindex="-1">
|
||||
{{buttonInfo.translationIdForText|translate}}
|
||||
</button>
|
||||
</li>
|
||||
@@ -102,13 +102,15 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
constructor: [
|
||||
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
blocklyApp.AudioService,
|
||||
function(
|
||||
_clipboardService, _notificationsService, _treeService,
|
||||
_utilsService) {
|
||||
_utilsService, _audioService) {
|
||||
this.clipboardService = _clipboardService;
|
||||
this.notificationsService = _notificationsService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
this.audioService = _audioService;
|
||||
}],
|
||||
getBlockDescription: function() {
|
||||
return this.utilsService.getBlockDescription(this.block);
|
||||
@@ -172,6 +174,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
var that = this;
|
||||
this.removeBlockAndSetFocus_(this.block, function() {
|
||||
that.block.dispose(true);
|
||||
that.audioService.playDeleteSound();
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
|
||||
@@ -47,7 +47,7 @@ blocklyApp.WorkspaceComponent = ng.core
|
||||
|
||||
<div *ngIf="workspace">
|
||||
<ol #tree *ngFor="#block of workspace.topBlocks_; #i = index"
|
||||
tabIndex="0" role="tree" class="blocklyTree blocklyWorkspaceTree"
|
||||
tabindex="0" role="tree" class="blocklyTree blocklyWorkspaceTree"
|
||||
[attr.aria-activedescendant]="getActiveDescId(tree.id)"
|
||||
[attr.aria-labelledby]="workspaceTitle.id"
|
||||
(keydown)="onKeypress($event, tree)">
|
||||
|
||||
@@ -657,8 +657,21 @@ Blockly.Flyout.prototype.show = function(xmlList) {
|
||||
contents.push({type: 'block', block: curBlock});
|
||||
var gap = parseInt(xml.getAttribute('gap'), 10);
|
||||
gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap);
|
||||
}
|
||||
else if (tagName == 'BUTTON') {
|
||||
} else if (xml.tagName.toUpperCase() == 'SEP') {
|
||||
// Change the gap between two blocks.
|
||||
// <sep gap="36"></sep>
|
||||
// The default gap is 24, can be set larger or smaller.
|
||||
// This overwrites the gap attribute on the previous block.
|
||||
// Note that a deprecated method is to add a gap to a block.
|
||||
// <block type="math_arithmetic" gap="8"></block>
|
||||
var newGap = parseInt(xml.getAttribute('gap'), 10);
|
||||
// Ignore gaps before the first block.
|
||||
if (!isNaN(newGap) && gaps.length > 0) {
|
||||
gaps[gaps.length - 1] = newGap;
|
||||
} else {
|
||||
gaps.push(this.MARGIN * 3);
|
||||
}
|
||||
} else if (tagName == 'BUTTON') {
|
||||
var label = xml.getAttribute('text');
|
||||
var curButton = new Blockly.FlyoutButton(this.workspace_,
|
||||
this.targetWorkspace_, label);
|
||||
|
||||
@@ -333,10 +333,8 @@ Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) {
|
||||
// Note that a deprecated method is to add a gap to a block.
|
||||
// <block type="math_arithmetic" gap="8"></block>
|
||||
var newGap = parseFloat(childIn.getAttribute('gap'));
|
||||
if (!isNaN(newGap)) {
|
||||
var oldGap = parseFloat(lastElement.getAttribute('gap'));
|
||||
var gap = isNaN(oldGap) ? newGap : oldGap + newGap;
|
||||
lastElement.setAttribute('gap', gap);
|
||||
if (!isNaN(newGap) && lastElement) {
|
||||
lastElement.setAttribute('gap', newGap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,8 +278,9 @@ Blockly.WorkspaceSvg.prototype.dispose = function() {
|
||||
this.zoomControls_ = null;
|
||||
}
|
||||
if (!this.options.parentWorkspace) {
|
||||
// Top-most workspace. Dispose of the SVG too.
|
||||
goog.dom.removeNode(this.getParentSvg());
|
||||
// Top-most workspace. Dispose of the div that the
|
||||
// svg is injected into (i.e. injectionDiv).
|
||||
goog.dom.removeNode(this.getParentSvg().parentNode);
|
||||
}
|
||||
if (this.resizeHandlerWrapper_) {
|
||||
Blockly.unbindEvent_(this.resizeHandlerWrapper_);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<script src="../../accessible/libs/angular2-all.umd.min.js"></script>
|
||||
|
||||
<script src="../../accessible/utils.service.js"></script>
|
||||
<script src="../../accessible/audio.service.js"></script>
|
||||
<script src="../../accessible/notifications.service.js"></script>
|
||||
<script src="../../accessible/clipboard.service.js"></script>
|
||||
<script src="../../accessible/tree.service.js"></script>
|
||||
@@ -31,7 +32,7 @@
|
||||
<script src="../../accessible/workspace.component.js"></script>
|
||||
<script src="../../accessible/app.component.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="../../media/accessible.css">
|
||||
<link rel="stylesheet" href="../../accessible/media/accessible.css">
|
||||
<style>
|
||||
body {
|
||||
background-color: #fff;
|
||||
@@ -48,8 +49,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="https://developers.google.com/blockly/">Blockly</a> >
|
||||
<a href="../index.html">Demos</a> > Accessible Blockly</h1>
|
||||
<h1>
|
||||
<a href="https://developers.google.com/blockly/">Blockly</a> >
|
||||
<a href="../index.html">Demos</a> > Accessible Blockly
|
||||
</h1>
|
||||
|
||||
<p>This is a simple demo of a version of Blockly designed for screen readers.</p>
|
||||
|
||||
@@ -70,7 +73,9 @@
|
||||
var ACCESSIBLE_GLOBALS = {
|
||||
// Additional buttons for the workspace toolbar that
|
||||
// go before the "Clear Workspace" button.
|
||||
toolbarButtonConfig: []
|
||||
toolbarButtonConfig: [],
|
||||
// Prefix of path to sound files.
|
||||
mediaPathPrefix: '../../accessible/media/'
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
ng.platform.browser.bootstrap(blocklyApp.AppView);
|
||||
|
||||
485
demos/blocklyfactory/app_controller.js
Normal file
485
demos/blocklyfactory/app_controller.js
Normal file
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview The AppController Class brings together the Block
|
||||
* Factory, Block Library, and Block Exporter functionality into a single web
|
||||
* app.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
goog.provide('AppController');
|
||||
|
||||
goog.require('BlockFactory');
|
||||
goog.require('BlockLibraryController');
|
||||
goog.require('BlockExporterController');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.string');
|
||||
|
||||
/**
|
||||
* Controller for the Blockly Factory
|
||||
* @constructor
|
||||
*/
|
||||
AppController = function() {
|
||||
// Initialize Block Library
|
||||
this.blockLibraryName = 'blockLibrary';
|
||||
this.blockLibraryController =
|
||||
new BlockLibraryController(this.blockLibraryName);
|
||||
this.blockLibraryController.populateBlockLibrary();
|
||||
|
||||
// Initialize Block Exporter
|
||||
this.exporter =
|
||||
new BlockExporterController(this.blockLibraryController.storage);
|
||||
|
||||
// Map of tab type to the div element for the tab.
|
||||
this.tabMap = {
|
||||
'BLOCK_FACTORY' : goog.dom.getElement('blockFactory_tab'),
|
||||
'WORKSPACE_FACTORY': goog.dom.getElement('workspaceFactory_tab'),
|
||||
'EXPORTER' : goog.dom.getElement('blocklibraryExporter_tab')
|
||||
};
|
||||
|
||||
// Selected tab.
|
||||
this.selectedTab = 'BLOCK_FACTORY';
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to the 'Import Block Library' button. Imports block library from file to
|
||||
* Block Factory. Expects user to upload a single file of JSON mapping each
|
||||
* block type to its xml text representation.
|
||||
*/
|
||||
AppController.prototype.importBlockLibraryFromFile = function() {
|
||||
var self = this;
|
||||
var files = document.getElementById('files');
|
||||
// If the file list is empty, the user likely canceled in the dialog.
|
||||
if (files.files.length > 0) {
|
||||
// The input tag doesn't have the "multiple" attribute
|
||||
// so the user can only choose 1 file.
|
||||
var file = files.files[0];
|
||||
var fileReader = new FileReader();
|
||||
|
||||
// Create a map of block type to xml text from the file when it has been
|
||||
// read.
|
||||
fileReader.addEventListener('load', function(event) {
|
||||
var fileContents = event.target.result;
|
||||
// Create empty object to hold the read block library information.
|
||||
var blockXmlTextMap = Object.create(null);
|
||||
try {
|
||||
// Parse the file to get map of block type to xml text.
|
||||
blockXmlTextMap = self.formatBlockLibForImport_(fileContents);
|
||||
} catch (e) {
|
||||
var message = 'Could not load your block library file.\n'
|
||||
window.alert(message + '\nFile Name: ' + file.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new block library storage object with inputted block library.
|
||||
var blockLibStorage = new BlockLibraryStorage(
|
||||
self.blockLibraryName, blockXmlTextMap);
|
||||
|
||||
// Update block library controller with the new block library
|
||||
// storage.
|
||||
self.blockLibraryController.setBlockLibStorage(blockLibStorage);
|
||||
// Update the block library dropdown.
|
||||
self.blockLibraryController.populateBlockLibrary();
|
||||
// Update the exporter's block library storage.
|
||||
self.exporter.setBlockLibStorage(blockLibStorage);
|
||||
});
|
||||
// Read the file.
|
||||
fileReader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to the 'Export Block Library' button. Exports block library to file that
|
||||
* contains JSON mapping each block type to its xml text representation.
|
||||
*/
|
||||
AppController.prototype.exportBlockLibraryToFile = function() {
|
||||
// Get map of block type to xml.
|
||||
var blockLib = this.blockLibraryController.getBlockLibrary();
|
||||
// Concatenate the xmls, each separated by a blank line.
|
||||
var blockLibText = this.formatBlockLibForExport_(blockLib);
|
||||
// Get file name.
|
||||
var filename = prompt('Enter the file name under which to save your block' +
|
||||
'library.');
|
||||
// Download file if all necessary parameters are provided.
|
||||
if (filename) {
|
||||
BlockFactory.createAndDownloadFile_(blockLibText, filename, 'xml');
|
||||
} else {
|
||||
alert('Could not export Block Library without file name under which to ' +
|
||||
'save library.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an object mapping block type to xml to text file for output.
|
||||
* @private
|
||||
*
|
||||
* @param {!Object} blockXmlMap - object mapping block type to xml
|
||||
* @return {string} String of each block's xml separated by a new line.
|
||||
*/
|
||||
AppController.prototype.formatBlockLibForExport_ = function(blockXmlMap) {
|
||||
var blockXmls = [];
|
||||
for (var blockType in blockXmlMap) {
|
||||
blockXmls.push(blockXmlMap[blockType]);
|
||||
}
|
||||
return blockXmls.join("\n\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts imported block library to an object mapping block type to block xml.
|
||||
* @private
|
||||
*
|
||||
* @param {string} xmlText - String containing each block's xml optionally
|
||||
* separated by whitespace.
|
||||
* @return {!Object} object mapping block type to xml text.
|
||||
*/
|
||||
AppController.prototype.formatBlockLibForImport_ = function(xmlText) {
|
||||
// Get array of xmls.
|
||||
var xmlText = goog.string.collapseWhitespace(xmlText);
|
||||
var blockXmls = goog.string.splitLimit(xmlText, '</xml>', 500);
|
||||
|
||||
// Create and populate map.
|
||||
var blockXmlTextMap = Object.create(null);
|
||||
// The line above is equivalent of {} except that this object is TRULY
|
||||
// empty. It doesn't have built-in attributes/functions such as length or
|
||||
// toString.
|
||||
for (var i = 0, xml; xml = blockXmls[i]; i++) {
|
||||
var blockType = this.getBlockTypeFromXml_(xml);
|
||||
blockXmlTextMap[blockType] = xml;
|
||||
}
|
||||
|
||||
return blockXmlTextMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts out block type from xml text, the kind that is saved in block
|
||||
* library storage.
|
||||
* @private
|
||||
*
|
||||
* @param {!string} xmlText - A block's xml text.
|
||||
* @return {string} The block type that corresponds to the provided xml text.
|
||||
*/
|
||||
AppController.prototype.getBlockTypeFromXml_ = function(xmlText) {
|
||||
var xmlText = Blockly.Options.parseToolboxTree(xmlText);
|
||||
// Find factory base block.
|
||||
var factoryBaseBlockXml = xmlText.getElementsByTagName('block')[0];
|
||||
// Get field elements from factory base.
|
||||
var fields = factoryBaseBlockXml.getElementsByTagName('field');
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
// The field whose name is 'NAME' holds the block type as its value.
|
||||
if (fields[i].getAttribute('name') == 'NAME') {
|
||||
return fields[i].childNodes[0].nodeValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the Block Factory tab to show selected block when user selects a
|
||||
* different block in the block library dropdown. Tied to block library dropdown
|
||||
* in index.html.
|
||||
*
|
||||
* @param {!Element} blockLibraryDropdown - HTML select element from which the
|
||||
* user selects a block to work on.
|
||||
*/
|
||||
AppController.prototype.onSelectedBlockChanged = function(blockLibraryDropdown) {
|
||||
// Get selected block type.
|
||||
var blockType = this.blockLibraryController.getSelectedBlockType(
|
||||
blockLibraryDropdown);
|
||||
// Update Block Factory page by showing the selected block.
|
||||
this.blockLibraryController.openBlock(blockType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add click handlers to each tab to allow switching between the Block Factory,
|
||||
* Workspace Factory, and Block Exporter tab.
|
||||
*
|
||||
* @param {!Object} tabMap - Map of tab name to div element that is the tab.
|
||||
*/
|
||||
AppController.prototype.addTabHandlers = function(tabMap) {
|
||||
var self = this;
|
||||
for (var tabName in tabMap) {
|
||||
var tab = tabMap[tabName];
|
||||
// Use an additional closure to correctly assign the tab callback.
|
||||
tab.addEventListener('click', self.makeTabClickHandler_(tabName));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the selected tab.
|
||||
* @private
|
||||
*
|
||||
* @param {string} tabName 'BLOCK_FACTORY', 'WORKSPACE_FACTORY', or 'EXPORTER'
|
||||
*/
|
||||
AppController.prototype.setSelected_ = function(tabName) {
|
||||
this.selectedTab = tabName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the tab click handler specific to the tab specified.
|
||||
* @private
|
||||
*
|
||||
* @param {string} tabName 'BLOCK_FACTORY', 'WORKSPACE_FACTORY', or 'EXPORTER'
|
||||
* @return {Function} The tab click handler.
|
||||
*/
|
||||
AppController.prototype.makeTabClickHandler_ = function(tabName) {
|
||||
var self = this;
|
||||
return function() {
|
||||
self.setSelected_(tabName);
|
||||
self.onTab();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Called on each tab click. Hides and shows specific content based on which tab
|
||||
* (Block Factory, Workspace Factory, or Exporter) is selected.
|
||||
*/
|
||||
AppController.prototype.onTab = function() {
|
||||
// Get tab div elements.
|
||||
var blockFactoryTab = this.tabMap['BLOCK_FACTORY'];
|
||||
var exporterTab = this.tabMap['EXPORTER'];
|
||||
var workspaceFactoryTab = this.tabMap['WORKSPACE_FACTORY'];
|
||||
|
||||
// Turn selected tab on and other tabs off.
|
||||
this.styleTabs_();
|
||||
|
||||
if (this.selectedTab == 'EXPORTER') {
|
||||
// Update toolbox to reflect current block library.
|
||||
this.exporter.updateToolbox();
|
||||
|
||||
// Show container of exporter.
|
||||
BlockFactory.show('blockLibraryExporter');
|
||||
BlockFactory.hide('workspaceFactoryContent');
|
||||
|
||||
} else if (this.selectedTab == 'BLOCK_FACTORY') {
|
||||
// Hide container of exporter.
|
||||
BlockFactory.hide('blockLibraryExporter');
|
||||
BlockFactory.hide('workspaceFactoryContent');
|
||||
|
||||
} else if (this.selectedTab == 'WORKSPACE_FACTORY') {
|
||||
// Hide container of exporter.
|
||||
BlockFactory.hide('blockLibraryExporter');
|
||||
// Show workspace factory container.
|
||||
BlockFactory.show('workspaceFactoryContent');
|
||||
}
|
||||
|
||||
// Resize to render workspaces' toolboxes correctly for all tabs.
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Called on each tab click. Styles the tabs to reflect which tab is selected.
|
||||
* @private
|
||||
*/
|
||||
AppController.prototype.styleTabs_ = function() {
|
||||
for (var tabName in this.tabMap) {
|
||||
if (this.selectedTab == tabName) {
|
||||
goog.dom.classlist.addRemove(this.tabMap[tabName], 'taboff', 'tabon');
|
||||
} else {
|
||||
goog.dom.classlist.addRemove(this.tabMap[tabName], 'tabon', 'taboff');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign button click handlers for the exporter.
|
||||
*/
|
||||
AppController.prototype.assignExporterClickHandlers = function() {
|
||||
var self = this;
|
||||
// Export blocks when the user submits the export settings.
|
||||
document.getElementById('exporterSubmitButton').addEventListener('click',
|
||||
function() {
|
||||
self.exporter.export();
|
||||
});
|
||||
document.getElementById('clearSelectedButton').addEventListener('click',
|
||||
function() {
|
||||
self.exporter.clearSelectedBlocks();
|
||||
});
|
||||
document.getElementById('addAllFromLibButton').addEventListener('click',
|
||||
function() {
|
||||
self.exporter.addAllBlocksToWorkspace();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign button click handlers for the block library.
|
||||
*/
|
||||
AppController.prototype.assignLibraryClickHandlers = function() {
|
||||
var self = this;
|
||||
// Assign button click handlers for Block Library.
|
||||
document.getElementById('saveToBlockLibraryButton').addEventListener('click',
|
||||
function() {
|
||||
self.blockLibraryController.saveToBlockLibrary();
|
||||
});
|
||||
|
||||
document.getElementById('removeBlockFromLibraryButton').addEventListener(
|
||||
'click',
|
||||
function() {
|
||||
self.blockLibraryController.removeFromBlockLibrary();
|
||||
});
|
||||
|
||||
document.getElementById('clearBlockLibraryButton').addEventListener('click',
|
||||
function() {
|
||||
self.blockLibraryController.clearBlockLibrary();
|
||||
});
|
||||
|
||||
var dropdown = document.getElementById('blockLibraryDropdown');
|
||||
dropdown.addEventListener('change',
|
||||
function() {
|
||||
self.onSelectedBlockChanged(dropdown);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign button click handlers for the block factory.
|
||||
*/
|
||||
AppController.prototype.assignFactoryClickHandlers = function() {
|
||||
var self = this;
|
||||
// Assign button event handlers for Block Factory.
|
||||
document.getElementById('localSaveButton')
|
||||
.addEventListener('click', function() {
|
||||
self.exportBlockLibraryToFile();
|
||||
});
|
||||
document.getElementById('helpButton').addEventListener('click',
|
||||
function() {
|
||||
open('https://developers.google.com/blockly/custom-blocks/block-factory',
|
||||
'BlockFactoryHelp');
|
||||
});
|
||||
document.getElementById('downloadBlocks').addEventListener('click',
|
||||
function() {
|
||||
BlockFactory.downloadTextArea('blocks', 'languagePre');
|
||||
});
|
||||
document.getElementById('downloadGenerator').addEventListener('click',
|
||||
function() {
|
||||
BlockFactory.downloadTextArea('generator', 'generatorPre');
|
||||
});
|
||||
document.getElementById('files').addEventListener('change',
|
||||
function() {
|
||||
// Warn user.
|
||||
var replace = confirm('This imported block library will ' +
|
||||
'replace your current block library.');
|
||||
if (replace) {
|
||||
self.importBlockLibraryFromFile();
|
||||
// Clear this so that the change event still fires even if the
|
||||
// same file is chosen again. If the user re-imports a file, we
|
||||
// want to reload the workspace with its contents.
|
||||
this.value = null;
|
||||
}
|
||||
});
|
||||
document.getElementById('createNewBlockButton')
|
||||
.addEventListener('click', function() {
|
||||
BlockFactory.mainWorkspace.clear();
|
||||
BlockFactory.showStarterBlock();
|
||||
BlockLibraryView.selectDefaultOption('blockLibraryDropdown');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add event listeners for the block factory.
|
||||
*/
|
||||
AppController.prototype.addFactoryEventListeners = function() {
|
||||
BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage);
|
||||
document.getElementById('direction')
|
||||
.addEventListener('change', BlockFactory.updatePreview);
|
||||
document.getElementById('languageTA')
|
||||
.addEventListener('change', BlockFactory.updatePreview);
|
||||
document.getElementById('languageTA')
|
||||
.addEventListener('keyup', BlockFactory.updatePreview);
|
||||
document.getElementById('format')
|
||||
.addEventListener('change', BlockFactory.formatChange);
|
||||
document.getElementById('language')
|
||||
.addEventListener('change', BlockFactory.updatePreview);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Blockly Storage with App Engine.
|
||||
*/
|
||||
AppController.prototype.initializeBlocklyStorage = function() {
|
||||
BlocklyStorage.HTTPREQUEST_ERROR =
|
||||
'There was a problem with the request.\n';
|
||||
BlocklyStorage.LINK_ALERT =
|
||||
'Share your blocks with this link:\n\n%1';
|
||||
BlocklyStorage.HASH_ERROR =
|
||||
'Sorry, "%1" doesn\'t correspond with any saved Blockly file.';
|
||||
BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n' +
|
||||
'Perhaps it was created with a different version of Blockly?';
|
||||
var linkButton = document.getElementById('linkButton');
|
||||
linkButton.style.display = 'inline-block';
|
||||
linkButton.addEventListener('click',
|
||||
function() {
|
||||
BlocklyStorage.link(BlockFactory.mainWorkspace);});
|
||||
BlockFactory.disableEnableLink();
|
||||
};
|
||||
/**
|
||||
* Initialize Blockly and layout. Called on page load.
|
||||
*/
|
||||
AppController.prototype.init = function() {
|
||||
// Handle Blockly Storage with App Engine
|
||||
if ('BlocklyStorage' in window) {
|
||||
this.initializeBlocklyStorage();
|
||||
}
|
||||
|
||||
// Assign click handlers.
|
||||
this.assignExporterClickHandlers();
|
||||
this.assignLibraryClickHandlers();
|
||||
this.assignFactoryClickHandlers();
|
||||
|
||||
// Handle resizing of Block Factory elements.
|
||||
var expandList = [
|
||||
document.getElementById('blockly'),
|
||||
document.getElementById('blocklyMask'),
|
||||
document.getElementById('preview'),
|
||||
document.getElementById('languagePre'),
|
||||
document.getElementById('languageTA'),
|
||||
document.getElementById('generatorPre')
|
||||
];
|
||||
|
||||
var onresize = function(e) {
|
||||
for (var i = 0, expand; expand = expandList[i]; i++) {
|
||||
expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
|
||||
expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
|
||||
}
|
||||
};
|
||||
onresize();
|
||||
window.addEventListener('resize', onresize);
|
||||
|
||||
// Inject Block Factory Main Workspace.
|
||||
var toolbox = document.getElementById('toolbox');
|
||||
BlockFactory.mainWorkspace = Blockly.inject('blockly',
|
||||
{collapse: false,
|
||||
toolbox: toolbox,
|
||||
media: '../../media/'});
|
||||
|
||||
// Add tab handlers for switching between Block Factory and Block Exporter.
|
||||
this.addTabHandlers(this.tabMap);
|
||||
|
||||
this.exporter.addChangeListenersToSelectorWorkspace();
|
||||
|
||||
// Create the root block on Block Factory main workspace.
|
||||
if ('BlocklyStorage' in window && window.location.hash.length > 1) {
|
||||
BlocklyStorage.retrieveXml(window.location.hash.substring(1),
|
||||
BlockFactory.mainWorkspace);
|
||||
} else {
|
||||
BlockFactory.showStarterBlock();
|
||||
}
|
||||
BlockFactory.mainWorkspace.clearUndo();
|
||||
|
||||
// Add Block Factory event listeners.
|
||||
this.addFactoryEventListeners();
|
||||
};
|
||||
300
demos/blocklyfactory/block_exporter_controller.js
Normal file
300
demos/blocklyfactory/block_exporter_controller.js
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Javascript for the Block Exporter Controller class. Allows
|
||||
* users to export block definitions and generator stubs of their saved blocks
|
||||
* easily using a visual interface. Depends on Block Exporter View and Block
|
||||
* Exporter Tools classes. Interacts with Export Settings in the index.html.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
goog.provide('BlockExporterController');
|
||||
goog.require('BlockExporterView');
|
||||
goog.require('BlockExporterTools');
|
||||
goog.require('goog.dom.xml');
|
||||
|
||||
/**
|
||||
* BlockExporter Controller Class
|
||||
* @constructor
|
||||
*
|
||||
* @param {!BlockLibrary.Storage} blockLibStorage - Block Library Storage.
|
||||
*/
|
||||
BlockExporterController = function(blockLibStorage) {
|
||||
// BlockLibrary.Storage object containing user's saved blocks
|
||||
this.blockLibStorage = blockLibStorage;
|
||||
// Utils for generating code to export
|
||||
this.tools = new BlockExporterTools();
|
||||
// View provides the selector workspace and export settings UI.
|
||||
this.view = new BlockExporterView(
|
||||
//Xml representation of the toolbox
|
||||
this.tools.generateToolboxFromLibrary(this.blockLibStorage));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the block library storage object from which exporter exports.
|
||||
*
|
||||
* @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object
|
||||
* that stores the blocks.
|
||||
*/
|
||||
BlockExporterController.prototype.setBlockLibStorage =
|
||||
function(blockLibStorage) {
|
||||
this.blockLibStorage = blockLibStorage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the block library storage object from which exporter exports.
|
||||
*
|
||||
* @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object
|
||||
* that stores the blocks.
|
||||
*/
|
||||
BlockExporterController.prototype.getBlockLibStorage =
|
||||
function(blockLibStorage) {
|
||||
return this.blockLibStorage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the selected block types.
|
||||
* @private
|
||||
*
|
||||
* @return {!Array.<string>} Types of blocks in workspace.
|
||||
*/
|
||||
BlockExporterController.prototype.getSelectedBlockTypes_ = function() {
|
||||
var selectedBlocks = this.view.getSelectedBlocks();
|
||||
var blockTypes = [];
|
||||
for (var i = 0, block; block = selectedBlocks[i]; i++) {
|
||||
blockTypes.push(block.type);
|
||||
}
|
||||
return blockTypes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get selected blocks from selector workspace, pulls info from the Export
|
||||
* Settings form in Block Exporter, and downloads code accordingly.
|
||||
*
|
||||
* TODO(quachtina96): allow export as zip.
|
||||
*/
|
||||
BlockExporterController.prototype.export = function() {
|
||||
// Get selected blocks' information.
|
||||
var blockTypes = this.getSelectedBlockTypes_();
|
||||
var blockXmlMap = this.blockLibStorage.getBlockXmlMap(blockTypes);
|
||||
|
||||
// Pull workspace-related settings from the Export Settings form.
|
||||
var wantToolbox = document.getElementById('toolboxCheck').checked;
|
||||
var wantPreloadedWorkspace =
|
||||
document.getElementById('preloadedWorkspaceCheck').checked;
|
||||
var wantWorkspaceOptions =
|
||||
document.getElementById('workspaceOptsCheck').checked;
|
||||
|
||||
// Pull block definition(s) settings from the Export Settings form.
|
||||
var wantBlockDef = document.getElementById('blockDefCheck').checked;
|
||||
var definitionFormat = document.getElementById('exportFormat').value;
|
||||
var blockDef_filename = document.getElementById('blockDef_filename').value;
|
||||
|
||||
// Pull block generator stub(s) settings from the Export Settings form.
|
||||
var wantGenStub = document.getElementById('genStubCheck').checked;
|
||||
var language = document.getElementById('exportLanguage').value;
|
||||
var generatorStub_filename = document.getElementById(
|
||||
'generatorStub_filename').value;
|
||||
|
||||
if (wantToolbox) {
|
||||
// TODO(quachtina96): create and download file once wfactory has been
|
||||
// integrated.
|
||||
}
|
||||
|
||||
if (wantPreloadedWorkspace) {
|
||||
// TODO(quachtina96): create and download file once wfactory has been
|
||||
// integrated.
|
||||
}
|
||||
|
||||
if (wantWorkspaceOptions) {
|
||||
// TODO(quachtina96): create and download file once wfactory has been
|
||||
// integrated.
|
||||
}
|
||||
|
||||
if (wantBlockDef) {
|
||||
// User wants to export selected blocks' definitions.
|
||||
if (!blockDef_filename) {
|
||||
// User needs to enter filename.
|
||||
alert('Please enter a filename for your block definition(s) download.');
|
||||
} else {
|
||||
// Get block definition code in the selected format for the blocks.
|
||||
var blockDefs = this.tools.getBlockDefs(blockXmlMap,
|
||||
definitionFormat);
|
||||
// Download the file.
|
||||
BlockFactory.createAndDownloadFile_(
|
||||
blockDefs, blockDef_filename, definitionFormat);
|
||||
}
|
||||
}
|
||||
|
||||
if (wantGenStub) {
|
||||
// User wants to export selected blocks' generator stubs.
|
||||
if (!generatorStub_filename) {
|
||||
// User needs to enter filename.
|
||||
alert('Please enter a filename for your generator stub(s) download.');
|
||||
} else {
|
||||
// Get generator stub code in the selected language for the blocks.
|
||||
var genStubs = this.tools.getGeneratorCode(blockXmlMap,
|
||||
language);
|
||||
// Download the file.
|
||||
BlockFactory.createAndDownloadFile_(
|
||||
genStubs, generatorStub_filename, language);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the Exporter's toolbox with either the given toolbox xml or toolbox
|
||||
* xml generated from blocks stored in block library.
|
||||
*
|
||||
* @param {Element} opt_toolboxXml - Xml to define toolbox of the selector
|
||||
* workspace.
|
||||
*/
|
||||
BlockExporterController.prototype.updateToolbox = function(opt_toolboxXml) {
|
||||
// Use given xml or xml generated from updated block library.
|
||||
var updatedToolbox = opt_toolboxXml ||
|
||||
this.tools.generateToolboxFromLibrary(this.blockLibStorage);
|
||||
// Update the view's toolbox.
|
||||
this.view.setToolbox(updatedToolbox);
|
||||
// Render the toolbox in the selector workspace.
|
||||
this.view.renderToolbox(updatedToolbox);
|
||||
// Disable any selected blocks.
|
||||
var selectedBlocks = this.getSelectedBlockTypes_();
|
||||
for (var i = 0, blockType; blockType = selectedBlocks[i]; i++) {
|
||||
this.setBlockEnabled(blockType, false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable or Disable block in selector workspace's toolbox.
|
||||
*
|
||||
* @param {!string} blockType - Type of block to disable or enable.
|
||||
* @param {!boolean} enable - True to enable the block, false to disable block.
|
||||
*/
|
||||
BlockExporterController.prototype.setBlockEnabled =
|
||||
function(blockType, enable) {
|
||||
// Get toolbox xml, category, and block elements.
|
||||
var toolboxXml = this.view.toolbox;
|
||||
var category = goog.dom.xml.selectSingleNode(toolboxXml,
|
||||
'//category[@name="' + blockType + '"]');
|
||||
var block = goog.dom.getFirstElementChild(category);
|
||||
// Enable block.
|
||||
goog.dom.xml.setAttributes(block, {disabled: !enable});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add change listeners to the exporter's selector workspace.
|
||||
*/
|
||||
BlockExporterController.prototype.addChangeListenersToSelectorWorkspace
|
||||
= function() {
|
||||
// Assign the BlockExporterController to 'self' to be called in the change
|
||||
// listeners. This keeps it in scope--otherwise, 'this' in the change
|
||||
// listeners refers to the wrong thing.
|
||||
var self = this;
|
||||
var selector = this.view.selectorWorkspace;
|
||||
selector.addChangeListener(
|
||||
function(event) {
|
||||
self.onSelectBlockForExport_(event);
|
||||
});
|
||||
selector.addChangeListener(
|
||||
function(event) {
|
||||
self.onDeselectBlockForExport_(event);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function for when a user selects a block for export in selector
|
||||
* workspace. Disables selected block so that the user only exports one
|
||||
* copy of starter code per block. Attached to the blockly create event in block
|
||||
* factory expansion's init.
|
||||
* @private
|
||||
*
|
||||
* @param {!Blockly.Events} event - The fired Blockly event.
|
||||
*/
|
||||
BlockExporterController.prototype.onSelectBlockForExport_ = function(event) {
|
||||
// The user created a block in selector workspace.
|
||||
if (event.type == Blockly.Events.CREATE) {
|
||||
// Get type of block created.
|
||||
var block = this.view.selectorWorkspace.getBlockById(event.blockId);
|
||||
var blockType = block.type;
|
||||
// Disable the selected block. Users can only export one copy of starter
|
||||
// code per block.
|
||||
this.setBlockEnabled(blockType, false);
|
||||
// Show currently selected blocks in helper text.
|
||||
this.view.listSelectedBlocks(this.getSelectedBlockTypes_());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function for when a user deselects a block in selector
|
||||
* workspace by deleting it. Re-enables block so that the user may select it for
|
||||
* export
|
||||
* @private
|
||||
*
|
||||
* @param {!Blockly.Events} event - The fired Blockly event.
|
||||
*/
|
||||
BlockExporterController.prototype.onDeselectBlockForExport_ = function(event) {
|
||||
// The user deleted a block in selector workspace.
|
||||
if (event.type == Blockly.Events.DELETE) {
|
||||
// Get type of block created.
|
||||
var deletedBlockXml = event.oldXml;
|
||||
var blockType = deletedBlockXml.getAttribute('type');
|
||||
// Enable the deselected block.
|
||||
this.setBlockEnabled(blockType, true);
|
||||
// Show currently selected blocks in helper text.
|
||||
this.view.listSelectedBlocks(this.getSelectedBlockTypes_());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to the 'Clear Selected Blocks' button in the Block Exporter.
|
||||
* Deselects all blocks on the selector workspace by deleting them and updating
|
||||
* text accordingly.
|
||||
*/
|
||||
BlockExporterController.prototype.clearSelectedBlocks = function() {
|
||||
// Clear selector workspace.
|
||||
this.view.clearSelectorWorkspace();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to the 'Add All Stored Blocks' button in the Block Exporter.
|
||||
* Adds all blocks stored in block library to the selector workspace.
|
||||
*/
|
||||
BlockExporterController.prototype.addAllBlocksToWorkspace = function() {
|
||||
// Clear selector workspace.
|
||||
this.view.clearSelectorWorkspace();
|
||||
|
||||
// Add and evaluate all blocks' definitions.
|
||||
var allBlockTypes = this.blockLibStorage.getBlockTypes();
|
||||
var blockXmlMap = this.blockLibStorage.getBlockXmlMap(allBlockTypes);
|
||||
this.tools.addBlockDefinitions(blockXmlMap);
|
||||
|
||||
// For every block, render in selector workspace.
|
||||
for (var i = 0, blockType; blockType = allBlockTypes[i]; i++) {
|
||||
this.view.addBlock(blockType);
|
||||
}
|
||||
|
||||
// Clean up workspace.
|
||||
this.view.cleanUpSelectorWorkspace();
|
||||
};
|
||||
223
demos/blocklyfactory/block_exporter_tools.js
Normal file
223
demos/blocklyfactory/block_exporter_tools.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Javascript for the BlockExporter Tools class, which generates
|
||||
* block definitions and generator stubs for given block types. Also generates
|
||||
* toolbox xml for the exporter's workspace. Depends on the BlockFactory for
|
||||
* its code generation functions.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.provide('BlockExporterTools');
|
||||
|
||||
goog.require('BlockFactory');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.xml');
|
||||
|
||||
/**
|
||||
* Block Exporter Tools Class
|
||||
* @constructor
|
||||
*/
|
||||
BlockExporterTools = function() {
|
||||
// Create container for hidden workspace.
|
||||
this.container = goog.dom.createDom('div', {
|
||||
'id': 'blockExporterTools_hiddenWorkspace'
|
||||
}, ''); // Empty quotes for empty div.
|
||||
// Hide hidden workspace.
|
||||
this.container.style.display = 'none';
|
||||
goog.dom.appendChild(document.body, this.container);
|
||||
/**
|
||||
* Hidden workspace for the Block Exporter that holds pieces that make
|
||||
* up the block
|
||||
* @type {Blockly.Workspace}
|
||||
*/
|
||||
this.hiddenWorkspace = Blockly.inject(this.container.id,
|
||||
{collapse: false,
|
||||
media: '../../media/'});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Blockly Block object from xml that encodes the blocks used to design
|
||||
* the block.
|
||||
* @private
|
||||
*
|
||||
* @param {!Element} xml - Xml element that encodes the blocks used to design
|
||||
* the block. For example, the block xmls saved in block library.
|
||||
* @return {!Blockly.Block} - Root block (factory_base block) which contains
|
||||
* all information needed to generate block definition or null.
|
||||
*/
|
||||
BlockExporterTools.prototype.getRootBlockFromXml_ = function(xml) {
|
||||
// Render xml in hidden workspace.
|
||||
this.hiddenWorkspace.clear();
|
||||
Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace);
|
||||
// Get root block.
|
||||
var rootBlock = this.hiddenWorkspace.getTopBlocks()[0] || null;
|
||||
return rootBlock;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Blockly Block by rendering pre-defined block in workspace.
|
||||
* @private
|
||||
*
|
||||
* @param {!Element} blockType - Type of block.
|
||||
* @return {!Blockly.Block} the Blockly.Block of desired type.
|
||||
*/
|
||||
BlockExporterTools.prototype.getDefinedBlock_ = function(blockType) {
|
||||
this.hiddenWorkspace.clear();
|
||||
return this.hiddenWorkspace.newBlock(blockType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the given language code of each block type in an array.
|
||||
*
|
||||
* @param {!Object} blockXmlMap - Map of block type to xml.
|
||||
* @param {string} definitionFormat - 'JSON' or 'JavaScript'
|
||||
* @return {string} The concatenation of each block's language code in the
|
||||
* desired format.
|
||||
*/
|
||||
BlockExporterTools.prototype.getBlockDefs =
|
||||
function(blockXmlMap, definitionFormat) {
|
||||
var blockCode = [];
|
||||
for (var blockType in blockXmlMap) {
|
||||
var xml = blockXmlMap[blockType];
|
||||
if (xml) {
|
||||
// Render and get block from hidden workspace.
|
||||
var rootBlock = this.getRootBlockFromXml_(xml);
|
||||
if (rootBlock) {
|
||||
// Generate the block's definition.
|
||||
var code = BlockFactory.getBlockDefinition(blockType, rootBlock,
|
||||
definitionFormat, this.hiddenWorkspace);
|
||||
// Add block's definition to the definitions to return.
|
||||
} else {
|
||||
// Append warning comment and write to console.
|
||||
var code = '// No block definition generated for ' + blockType +
|
||||
'. Could not find root block in xml stored for this block.';
|
||||
console.log('No block definition generated for ' + blockType +
|
||||
'. Could not find root block in xml stored for this block.');
|
||||
}
|
||||
} else {
|
||||
// Append warning comment and write to console.
|
||||
var code = '// No block definition generated for ' + blockType +
|
||||
'. Block was not found in Block Library Storage.';
|
||||
console.log('No block definition generated for ' + blockType +
|
||||
'. Block was not found in Block Library Storage.');
|
||||
}
|
||||
blockCode.push(code);
|
||||
}
|
||||
return blockCode.join("\n\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the generator code of each block type in an array in a given language.
|
||||
*
|
||||
* @param {!Object} blockXmlMap - Map of block type to xml.
|
||||
* @param {string} generatorLanguage - e.g.'JavaScript', 'Python', 'PHP', 'Lua',
|
||||
* 'Dart'
|
||||
* @return {string} The concatenation of each block's generator code in the
|
||||
* desired format.
|
||||
*/
|
||||
BlockExporterTools.prototype.getGeneratorCode =
|
||||
function(blockXmlMap, generatorLanguage) {
|
||||
var multiblockCode = [];
|
||||
// Define the custom blocks in order to be able to create instances of
|
||||
// them in the exporter workspace.
|
||||
this.addBlockDefinitions(blockXmlMap);
|
||||
|
||||
for (var blockType in blockXmlMap) {
|
||||
var xml = blockXmlMap[blockType];
|
||||
if (xml) {
|
||||
// Render the preview block in the hidden workspace.
|
||||
var tempBlock = this.getDefinedBlock_(blockType);
|
||||
// Get generator stub for the given block and add to generator code.
|
||||
var blockGenCode =
|
||||
BlockFactory.getGeneratorStub(tempBlock, generatorLanguage);
|
||||
} else {
|
||||
// Append warning comment and write to console.
|
||||
var blockGenCode = '// No generator stub generated for ' + blockType +
|
||||
'. Block was not found in Block Library Storage.';
|
||||
console.log('No block generator stub generated for ' + blockType +
|
||||
'. Block was not found in Block Library Storage.');
|
||||
}
|
||||
multiblockCode.push(blockGenCode);
|
||||
}
|
||||
return multiblockCode.join("\n\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluates block definition code of each block in given object mapping
|
||||
* block type to xml. Called in order to be able to create instances of the
|
||||
* blocks in the exporter workspace.
|
||||
*
|
||||
* @param {!Object} blockXmlMap - Map of block type to xml.
|
||||
*/
|
||||
BlockExporterTools.prototype.addBlockDefinitions = function(blockXmlMap) {
|
||||
var blockDefs = this.getBlockDefs(blockXmlMap, 'JavaScript');
|
||||
eval(blockDefs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pulls information about all blocks in the block library to generate xml
|
||||
* for the selector workpace's toolbox.
|
||||
*
|
||||
* @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object.
|
||||
* @return {!Element} Xml representation of the toolbox.
|
||||
*/
|
||||
BlockExporterTools.prototype.generateToolboxFromLibrary
|
||||
= function(blockLibStorage) {
|
||||
// Create DOM for XML.
|
||||
var xmlDom = goog.dom.createDom('xml', {
|
||||
'id' : 'blockExporterTools_toolbox',
|
||||
'style' : 'display:none'
|
||||
});
|
||||
|
||||
var allBlockTypes = blockLibStorage.getBlockTypes();
|
||||
// Object mapping block type to XML.
|
||||
var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes);
|
||||
|
||||
// Define the custom blocks in order to be able to create instances of
|
||||
// them in the exporter workspace.
|
||||
this.addBlockDefinitions(blockXmlMap);
|
||||
|
||||
for (var blockType in blockXmlMap) {
|
||||
// Create category DOM element.
|
||||
var categoryElement = goog.dom.createDom('category');
|
||||
categoryElement.setAttribute('name',blockType);
|
||||
|
||||
// Get block.
|
||||
var block = this.getDefinedBlock_(blockType);
|
||||
|
||||
// Get preview block XML.
|
||||
var blockChild = Blockly.Xml.blockToDom(block);
|
||||
blockChild.removeAttribute('id');
|
||||
|
||||
// Add block to category and category to XML.
|
||||
categoryElement.appendChild(blockChild);
|
||||
xmlDom.appendChild(categoryElement);
|
||||
}
|
||||
|
||||
// If there are no blocks in library, append dummy category.
|
||||
var categoryElement = goog.dom.createDom('category');
|
||||
categoryElement.setAttribute('name','Next Saved Block');
|
||||
xmlDom.appendChild(categoryElement);
|
||||
return xmlDom;
|
||||
};
|
||||
145
demos/blocklyfactory/block_exporter_view.js
Normal file
145
demos/blocklyfactory/block_exporter_view.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Javascript for the Block Exporter View class. Takes care of
|
||||
* generating the selector workspace through which users select blocks to
|
||||
* export.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
goog.provide('BlockExporterView');
|
||||
|
||||
goog.require('goog.dom');
|
||||
|
||||
/**
|
||||
* BlockExporter View Class
|
||||
* @constructor
|
||||
*
|
||||
* @param {Element} toolbox - Xml for the toolbox of the selector workspace.
|
||||
*/
|
||||
BlockExporterView = function(toolbox) {
|
||||
// Xml representation of the toolbox
|
||||
if (toolbox.hasChildNodes) {
|
||||
this.toolbox = toolbox;
|
||||
} else {
|
||||
// Toolbox is empty. Append dummy category to toolbox because toolbox
|
||||
// cannot switch between category and flyout-only mode after injection.
|
||||
var categoryElement = goog.dom.createDom('category');
|
||||
categoryElement.setAttribute('name', 'Next Saved Block');
|
||||
toolbox.appendChild(categoryElement);
|
||||
this.toolbox = toolbox;
|
||||
}
|
||||
// Workspace users use to select blocks for export
|
||||
this.selectorWorkspace =
|
||||
Blockly.inject('exportSelector',
|
||||
{collapse: false,
|
||||
toolbox: this.toolbox,
|
||||
grid:
|
||||
{spacing: 20,
|
||||
length: 3,
|
||||
colour: '#ccc',
|
||||
snap: true}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the toolbox of this instance of BlockExporterView.
|
||||
*
|
||||
* @param {Element} toolboxXml - Xml for the toolbox of the selector workspace.
|
||||
*/
|
||||
BlockExporterView.prototype.setToolbox = function(toolboxXml) {
|
||||
// Parse the provided toolbox tree into a consistent DOM format.
|
||||
this.toolbox = Blockly.Options.parseToolboxTree(toolboxXml);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the toolbox in the workspace. Used to update the toolbox upon
|
||||
* switching between Block Factory tab and Block Exporter Tab.
|
||||
*/
|
||||
BlockExporterView.prototype.renderToolbox = function() {
|
||||
this.selectorWorkspace.updateToolbox(this.toolbox);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the helper text.
|
||||
*
|
||||
* @param {string} newText - New helper text.
|
||||
* @param {boolean} opt_append - True if appending to helper Text, false if
|
||||
* replacing.
|
||||
*/
|
||||
BlockExporterView.prototype.updateHelperText = function(newText, opt_append) {
|
||||
if (opt_append) {
|
||||
goog.dom.getElement('helperText').textContent =
|
||||
goog.dom.getElement('helperText').textContent + newText;
|
||||
} else {
|
||||
goog.dom.getElement('helperText').textContent = newText;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the helper text to show list of currently selected blocks.
|
||||
*
|
||||
* @param {!Array.<string>} selectedBlockTypes - Array of blocks selected in workspace.
|
||||
*/
|
||||
BlockExporterView.prototype.listSelectedBlocks = function(selectedBlockTypes) {
|
||||
var selectedBlocksText = selectedBlockTypes.join(', ');
|
||||
this.updateHelperText('Currently Selected: ' + selectedBlocksText);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders block of given type on selector workspace assuming block has already
|
||||
* been defined.
|
||||
*
|
||||
* @param {string} blockType - Type of block to add to selector workspce.
|
||||
*/
|
||||
BlockExporterView.prototype.addBlock = function(blockType) {
|
||||
var newBlock = this.selectorWorkspace.newBlock(blockType);
|
||||
newBlock.initSvg();
|
||||
newBlock.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears selector workspace.
|
||||
*/
|
||||
BlockExporterView.prototype.clearSelectorWorkspace = function() {
|
||||
this.selectorWorkspace.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Neatly layout the blocks in selector workspace.
|
||||
*/
|
||||
BlockExporterView.prototype.cleanUpSelectorWorkspace = function() {
|
||||
this.selectorWorkspace.cleanUp_();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns array of selected blocks.
|
||||
*
|
||||
* @return {Array.<Blockly.Block>} Array of all blocks in selector workspace.
|
||||
*/
|
||||
BlockExporterView.prototype.getSelectedBlocks = function() {
|
||||
return this.selectorWorkspace.getAllBlocks();
|
||||
};
|
||||
|
||||
|
||||
219
demos/blocklyfactory/block_library_controller.js
Normal file
219
demos/blocklyfactory/block_library_controller.js
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Contains the code for Block Library Controller, which
|
||||
* depends on Block Library Storage and Block Library UI. Provides the
|
||||
* interfaces for the user to
|
||||
* - save their blocks to the browser
|
||||
* - re-open and edit saved blocks
|
||||
* - delete blocks
|
||||
* - clear their block library
|
||||
* Depends on BlockFactory functions defined in factory.js.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
goog.provide('BlockLibraryController');
|
||||
|
||||
goog.require('BlockLibraryStorage');
|
||||
goog.require('BlockLibraryView');
|
||||
goog.require('BlockFactory');
|
||||
|
||||
/**
|
||||
* Block Library Controller Class
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} blockLibraryName - Desired name of Block Library, also used
|
||||
* to create the key for where it's stored in local storage.
|
||||
* @param {!BlockLibraryStorage} opt_blockLibraryStorage - optional storage
|
||||
* object that allows user to import a block library.
|
||||
*/
|
||||
BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) {
|
||||
this.name = blockLibraryName;
|
||||
// Create a new, empty Block Library Storage object, or load existing one.
|
||||
this.storage = opt_blockLibraryStorage || new BlockLibraryStorage(this.name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the block type of the block the user is building.
|
||||
* @private
|
||||
*
|
||||
* @return {string} The current block's type.
|
||||
*/
|
||||
BlockLibraryController.prototype.getCurrentBlockType_ = function() {
|
||||
var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace);
|
||||
var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
|
||||
// Replace white space with underscores
|
||||
return blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes current block from Block Library
|
||||
*
|
||||
* @param {string} blockType - Type of block.
|
||||
*/
|
||||
BlockLibraryController.prototype.removeFromBlockLibrary = function() {
|
||||
var blockType = this.getCurrentBlockType_();
|
||||
this.storage.removeBlock(blockType);
|
||||
this.storage.saveToLocalStorage();
|
||||
this.populateBlockLibrary();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the workspace to show the block user selected from library
|
||||
*
|
||||
* @param {string} blockType - Block to edit on block factory.
|
||||
*/
|
||||
BlockLibraryController.prototype.openBlock = function(blockType) {
|
||||
var xml = this.storage.getBlockXml(blockType);
|
||||
BlockFactory.mainWorkspace.clear();
|
||||
Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns type of block selected from library.
|
||||
*
|
||||
* @param {Element} blockLibraryDropdown - The block library dropdown.
|
||||
* @return {string} Type of block selected.
|
||||
*/
|
||||
BlockLibraryController.prototype.getSelectedBlockType =
|
||||
function(blockLibraryDropdown) {
|
||||
return BlockLibraryView.getSelected(blockLibraryDropdown);
|
||||
};
|
||||
|
||||
/**
|
||||
* Confirms with user before clearing the block library in local storage and
|
||||
* updating the dropdown.
|
||||
*/
|
||||
BlockLibraryController.prototype.clearBlockLibrary = function() {
|
||||
var check = confirm(
|
||||
'Click OK to clear your block library.');
|
||||
if (check) {
|
||||
// Clear Block Library Storage.
|
||||
this.storage.clear();
|
||||
this.storage.saveToLocalStorage();
|
||||
// Update dropdown.
|
||||
BlockLibraryView.clearOptions('blockLibraryDropdown');
|
||||
// Add a default, blank option to dropdown for when no block from library is
|
||||
// selected.
|
||||
BlockLibraryView.addDefaultOption('blockLibraryDropdown');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves current block to local storage and updates dropdown.
|
||||
*/
|
||||
BlockLibraryController.prototype.saveToBlockLibrary = function() {
|
||||
var blockType = this.getCurrentBlockType_();
|
||||
// If block under that name already exists, confirm that user wants to replace
|
||||
// saved block.
|
||||
if (this.isInBlockLibrary(blockType)) {
|
||||
var replace = confirm('You already have a block called ' + blockType +
|
||||
' in your library. Click OK to replace.');
|
||||
if (!replace) {
|
||||
// Do not save if user doesn't want to replace the saved block.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save block.
|
||||
var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace);
|
||||
this.storage.addBlock(blockType, xmlElement);
|
||||
this.storage.saveToLocalStorage();
|
||||
|
||||
// Do not add another option to dropdown if replacing.
|
||||
if (replace) {
|
||||
return;
|
||||
}
|
||||
BlockLibraryView.addOption(
|
||||
blockType, blockType, 'blockLibraryDropdown', true, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if the given blockType is already in Block Library
|
||||
*
|
||||
* @param {string} blockType - Type of block.
|
||||
* @return {boolean} Boolean indicating whether or not block is in the library.
|
||||
*/
|
||||
BlockLibraryController.prototype.isInBlockLibrary = function(blockType) {
|
||||
var blockLibrary = this.storage.blocks;
|
||||
return (blockType in blockLibrary && blockLibrary[blockType] != null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Populates the dropdown menu.
|
||||
*/
|
||||
BlockLibraryController.prototype.populateBlockLibrary = function() {
|
||||
BlockLibraryView.clearOptions('blockLibraryDropdown');
|
||||
// Add a default, blank option to dropdown for when no block from library is
|
||||
// selected.
|
||||
BlockLibraryView.addDefaultOption('blockLibraryDropdown');
|
||||
// Add option for each saved block.
|
||||
var blockLibrary = this.storage.blocks;
|
||||
for (var block in blockLibrary) {
|
||||
// Make sure the block wasn't deleted.
|
||||
if (blockLibrary[block] != null) {
|
||||
BlockLibraryView.addOption(
|
||||
block, block, 'blockLibraryDropdown', false, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return block library mapping block type to xml.
|
||||
*
|
||||
* @return {Object} Object mapping block type to xml text.
|
||||
*/
|
||||
BlockLibraryController.prototype.getBlockLibrary = function() {
|
||||
return this.storage.getBlockXmlTextMap();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the block library storage object from which exporter exports.
|
||||
*
|
||||
* @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage
|
||||
* object.
|
||||
*/
|
||||
BlockLibraryController.prototype.setBlockLibStorage
|
||||
= function(blockLibStorage) {
|
||||
this.storage = blockLibStorage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the block library storage object from which exporter exports.
|
||||
*
|
||||
* @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object
|
||||
* that stores the blocks.
|
||||
*/
|
||||
BlockLibraryController.prototype.getBlockLibStorage =
|
||||
function(blockLibStorage) {
|
||||
return this.blockLibStorage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the block library storage object from which exporter exports.
|
||||
*
|
||||
* @return {boolean} True if the Block Library is empty, false otherwise.
|
||||
*/
|
||||
BlockLibraryController.prototype.hasEmptyBlockLib = function() {
|
||||
return this.storage.isEmpty();
|
||||
};
|
||||
167
demos/blocklyfactory/block_library_storage.js
Normal file
167
demos/blocklyfactory/block_library_storage.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Javascript for Block Library's Storage Class.
|
||||
* Depends on Block Library for its namespace.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
goog.provide('BlockLibraryStorage');
|
||||
|
||||
/**
|
||||
* Represents a block library's storage.
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} blockLibraryName - Desired name of Block Library, also used
|
||||
* to create the key for where it's stored in local storage.
|
||||
* @param {Object} opt_blocks - Object mapping block type to xml.
|
||||
*/
|
||||
BlockLibraryStorage = function(blockLibraryName, opt_blocks) {
|
||||
// Add prefix to this.name to avoid collisions in local storage.
|
||||
this.name = 'BlockLibraryStorage.' + blockLibraryName;
|
||||
if (!opt_blocks) {
|
||||
// Initialize this.blocks by loading from local storage.
|
||||
this.loadFromLocalStorage();
|
||||
if (this.blocks == null) {
|
||||
this.blocks = Object.create(null);
|
||||
// The line above is equivalent of {} except that this object is TRULY
|
||||
// empty. It doesn't have built-in attributes/functions such as length or
|
||||
// toString.
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
} else {
|
||||
this.blocks = opt_blocks;
|
||||
this.saveToLocalStorage();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the named block library from local storage and saves it in this.blocks.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.loadFromLocalStorage = function() {
|
||||
// goog.global is synonymous to window, and allows for flexibility
|
||||
// between browsers.
|
||||
var object = goog.global.localStorage[this.name];
|
||||
this.blocks = object ? JSON.parse(object) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes the current block library (this.blocks) to local storage.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.saveToLocalStorage = function() {
|
||||
goog.global.localStorage[this.name] = JSON.stringify(this.blocks);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the current block library.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.clear = function() {
|
||||
this.blocks = Object.create(null);
|
||||
// The line above is equivalent of {} except that this object is TRULY
|
||||
// empty. It doesn't have built-in attributes/functions such as length or
|
||||
// toString.
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves block to block library.
|
||||
*
|
||||
* @param {string} blockType - Type of block.
|
||||
* @param {Element} blockXML - The block's XML pulled from workspace.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.addBlock = function(blockType, blockXML) {
|
||||
var prettyXml = Blockly.Xml.domToPrettyText(blockXML);
|
||||
this.blocks[blockType] = prettyXml;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes block from current block library (this.blocks).
|
||||
*
|
||||
* @param {string} blockType - Type of block.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.removeBlock = function(blockType) {
|
||||
delete this.blocks[blockType];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the xml of given block type stored in current block library
|
||||
* (this.blocks).
|
||||
*
|
||||
* @param {string} blockType - Type of block.
|
||||
* @return {Element} The xml that represents the block type or null.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.getBlockXml = function(blockType) {
|
||||
var xml = this.blocks[blockType] || null;
|
||||
if (xml) {
|
||||
var xml = Blockly.Xml.textToDom(xml);
|
||||
}
|
||||
return xml;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns map of each block type to its corresponding xml stored in current
|
||||
* block library (this.blocks).
|
||||
*
|
||||
* @param {Array.<!string>} blockTypes - Types of blocks.
|
||||
* @return {!Object} Map of block type to corresponding xml.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.getBlockXmlMap = function(blockTypes) {
|
||||
var blockXmlMap = {};
|
||||
for (var i = 0; i < blockTypes.length; i++) {
|
||||
var blockType = blockTypes[i];
|
||||
var xml = this.getBlockXml(blockType);
|
||||
blockXmlMap[blockType] = xml;
|
||||
}
|
||||
return blockXmlMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns array of all block types stored in current block library.
|
||||
*
|
||||
* @return {!Array.<string>} Array of block types stored in library.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.getBlockTypes = function() {
|
||||
return Object.keys(this.blocks);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks to see if block library is empty.
|
||||
*
|
||||
* @return {boolean} True if empty, false otherwise.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.isEmpty = function() {
|
||||
for (var blockType in this.blocks) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns array of all block types stored in current block library.
|
||||
*
|
||||
* @return {!Array.<string>} Map of block type to corresponding xml text.
|
||||
*/
|
||||
BlockLibraryStorage.prototype.getBlockXmlTextMap = function() {
|
||||
return this.blocks;
|
||||
};
|
||||
117
demos/blocklyfactory/block_library_view.js
Normal file
117
demos/blocklyfactory/block_library_view.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Javascript for Block Library's UI for pulling blocks from the
|
||||
* Block Library's storage to edit in Block Factory.
|
||||
*
|
||||
* @author quachtina96 (Tina Quach)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
goog.provide('BlockLibraryView');
|
||||
|
||||
/**
|
||||
* Creates a node of a given element type and appends to the node with given id.
|
||||
*
|
||||
* @param {string} optionIdentifier - String used to identify option.
|
||||
* @param {string} optionText - Text to display in the dropdown for the option.
|
||||
* @param {string} dropdownID - ID for HTML select element.
|
||||
* @param {boolean} selected - Whether or not the option should be selected on
|
||||
* the dropdown.
|
||||
* @param {boolean} enabled - Whether or not the option should be enabled.
|
||||
*/
|
||||
BlockLibraryView.addOption
|
||||
= function(optionIdentifier, optionText, dropdownID, selected, enabled) {
|
||||
var dropdown = document.getElementById(dropdownID);
|
||||
var option = document.createElement('option');
|
||||
// The value attribute of a dropdown's option is not visible in the UI, but is
|
||||
// useful for identifying different options that may have the same text.
|
||||
option.value = optionIdentifier;
|
||||
// The text attribute is what the user sees in the dropdown for the option.
|
||||
option.text = optionText;
|
||||
option.selected = selected;
|
||||
option.disabled = !enabled;
|
||||
dropdown.add(option);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a default, blank option to dropdown for when no block from library is
|
||||
* selected.
|
||||
*
|
||||
* @param {string} dropdownID - ID of HTML select element
|
||||
*/
|
||||
BlockLibraryView.addDefaultOption = function(dropdownID) {
|
||||
BlockLibraryView.addOption(
|
||||
'BLOCK_LIBRARY_DEFAULT_BLANK', '', dropdownID, true, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects the default, blank option in dropdown identified by given ID.
|
||||
*
|
||||
* @param {string} dropdownID - ID of HTML select element
|
||||
*/
|
||||
BlockLibraryView.selectDefaultOption = function(dropdownID) {
|
||||
var dropdown = document.getElementById(dropdownID);
|
||||
// Deselect currently selected option.
|
||||
var index = dropdown.selectedIndex;
|
||||
dropdown.options[index].selected = false;
|
||||
// Select default option, always the first in the dropdown.
|
||||
var defaultOption = dropdown.options[0];
|
||||
defaultOption.selected = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns block type of selected block.
|
||||
*
|
||||
* @param {Element} dropdown - HTML select element.
|
||||
* @return {string} Type of block selected.
|
||||
*/
|
||||
BlockLibraryView.getSelected = function(dropdown) {
|
||||
var index = dropdown.selectedIndex;
|
||||
return dropdown.options[index].value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes option currently selected in dropdown from dropdown menu.
|
||||
*
|
||||
* @param {string} dropdownID - ID of HTML select element within which to find
|
||||
* the selected option.
|
||||
*/
|
||||
BlockLibraryView.removeSelectedOption = function(dropdownID) {
|
||||
var dropdown = document.getElementById(dropdownID);
|
||||
if (dropdown) {
|
||||
dropdown.remove(dropdown.selectedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all options from dropdown.
|
||||
*
|
||||
* @param {string} dropdownID - ID of HTML select element to clear options of.
|
||||
*/
|
||||
BlockLibraryView.clearOptions = function(dropdownID) {
|
||||
var dropdown = document.getElementById(dropdownID);
|
||||
while (dropdown.length > 0) {
|
||||
dropdown.remove(dropdown.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
217
demos/blocklyfactory/factory.css
Normal file
217
demos/blocklyfactory/factory.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
margin: 0 5px;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
-webkit-margin-before: 0em;
|
||||
-webkit-margin-after: 0em;
|
||||
-webkit-margin-start: 0px;
|
||||
-webkit-margin-end: 0px;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
|
||||
|
||||
#blockly {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#blocklyMask {
|
||||
background-color: #000;
|
||||
cursor: not-allowed;
|
||||
display: none;
|
||||
position: fixed;
|
||||
opacity: 0.2;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
#preview {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
pre,
|
||||
#languageTA {
|
||||
border: #ddd 1px solid;
|
||||
margin-top: 0;
|
||||
position: absolute;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
#languageTA {
|
||||
display: none;
|
||||
font: 10pt monospace;
|
||||
}
|
||||
|
||||
.downloadButton {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
button:disabled, .buttonStyle:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
button>*, .buttonStyle>* {
|
||||
opacity: 1;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
button, .buttonStyle {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #eee;
|
||||
color: #000;
|
||||
padding: 10px;
|
||||
margin: 10px 5px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.buttonStyle:hover:not(:disabled), button:hover:not(:disabled) {
|
||||
box-shadow: 2px 2px 5px #888;
|
||||
}
|
||||
|
||||
.buttonStyle:hover:not(:disabled)>*, button:hover:not(:disabled)>* {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#linkButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#blockFactoryContent {
|
||||
height: 87%;
|
||||
}
|
||||
|
||||
#blockLibraryContainer {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
#blockLibraryControls {
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#previewContainer {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
#buttonContainer {
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#files {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#toolbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#blocklyWorkspaceContainer {
|
||||
height: 95%;
|
||||
padding: 2px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/* Workspace Factory */
|
||||
|
||||
#workspaceFactoryContent {
|
||||
clear: both;
|
||||
display: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Exporter */
|
||||
|
||||
#blockLibraryExporter {
|
||||
clear: both;
|
||||
display: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#exportSelector {
|
||||
float: left;
|
||||
height: 75%;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#exportSettings {
|
||||
float: left;
|
||||
padding: 16px;
|
||||
width: 30%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#exporterHiddenWorkspace {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#exporterPreview {
|
||||
float: right;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
|
||||
.tab {
|
||||
float: left;
|
||||
padding: 5px 19px;
|
||||
}
|
||||
|
||||
.tab.tabon {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.tab.taboff {
|
||||
cursor: pointer;
|
||||
}
|
||||
989
demos/blocklyfactory/factory.js
Normal file
989
demos/blocklyfactory/factory.js
Normal file
@@ -0,0 +1,989 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview JavaScript for Blockly's Block Factory application through
|
||||
* which users can build blocks using a visual interface and dynamically
|
||||
* generate a preview block and starter code for the block (block definition and
|
||||
* generator stub. Uses the Block Factory namespace.
|
||||
*
|
||||
* @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach)
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Namespace for Block Factory.
|
||||
*/
|
||||
goog.provide('BlockFactory');
|
||||
|
||||
goog.require('goog.dom.classes');
|
||||
|
||||
/**
|
||||
* Workspace for user to build block.
|
||||
* @type {Blockly.Workspace}
|
||||
*/
|
||||
BlockFactory.mainWorkspace = null;
|
||||
|
||||
/**
|
||||
* Workspace for preview of block.
|
||||
* @type {Blockly.Workspace}
|
||||
*/
|
||||
BlockFactory.previewWorkspace = null;
|
||||
|
||||
/**
|
||||
* Name of block if not named.
|
||||
*/
|
||||
BlockFactory.UNNAMED = 'unnamed';
|
||||
|
||||
/**
|
||||
* Existing direction ('ltr' vs 'rtl') of preview.
|
||||
*/
|
||||
BlockFactory.oldDir = null;
|
||||
|
||||
// UI
|
||||
|
||||
/**
|
||||
* Inject code into a pre tag, with syntax highlighting.
|
||||
* Safe from HTML/script injection.
|
||||
* @param {string} code Lines of code.
|
||||
* @param {string} id ID of <pre> element to inject into.
|
||||
*/
|
||||
BlockFactory.injectCode = function(code, id) {
|
||||
var pre = document.getElementById(id);
|
||||
pre.textContent = code;
|
||||
code = pre.innerHTML;
|
||||
code = prettyPrintOne(code, 'js');
|
||||
pre.innerHTML = code;
|
||||
};
|
||||
|
||||
// Utils
|
||||
|
||||
/**
|
||||
* Escape a string.
|
||||
* @param {string} string String to escape.
|
||||
* @return {string} Escaped string surrouned by quotes.
|
||||
*/
|
||||
BlockFactory.escapeString = function(string) {
|
||||
return JSON.stringify(string);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the uneditable container block that everything else attaches to in
|
||||
* given workspace
|
||||
*
|
||||
* @param {!Blockly.Workspace} workspace - where the root block lives
|
||||
* @return {Blockly.Block} root block
|
||||
*/
|
||||
BlockFactory.getRootBlock = function(workspace) {
|
||||
var blocks = workspace.getTopBlocks(false);
|
||||
for (var i = 0, block; block = blocks[i]; i++) {
|
||||
if (block.type == 'factory_base') {
|
||||
return block;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Language Code: Block Definitions
|
||||
|
||||
/**
|
||||
* Change the language code format.
|
||||
*/
|
||||
BlockFactory.formatChange = function() {
|
||||
var mask = document.getElementById('blocklyMask');
|
||||
var languagePre = document.getElementById('languagePre');
|
||||
var languageTA = document.getElementById('languageTA');
|
||||
if (document.getElementById('format').value == 'Manual') {
|
||||
Blockly.hideChaff();
|
||||
mask.style.display = 'block';
|
||||
languagePre.style.display = 'none';
|
||||
languageTA.style.display = 'block';
|
||||
var code = languagePre.textContent.trim();
|
||||
languageTA.value = code;
|
||||
languageTA.focus();
|
||||
BlockFactory.updatePreview();
|
||||
} else {
|
||||
mask.style.display = 'none';
|
||||
languageTA.style.display = 'none';
|
||||
languagePre.style.display = 'block';
|
||||
BlockFactory.updateLanguage();
|
||||
}
|
||||
BlockFactory.disableEnableLink();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get block definition code for the current block.
|
||||
*
|
||||
* @param {string} blockType - Type of block.
|
||||
* @param {!Blockly.Block} rootBlock - RootBlock from main workspace in which
|
||||
* user uses Block Factory Blocks to create a custom block.
|
||||
* @param {string} format - 'JSON' or 'JavaScript'.
|
||||
* @param {!Blockly.Workspace} workspace - Where the root block lives.
|
||||
* @return {string} Block definition.
|
||||
*/
|
||||
BlockFactory.getBlockDefinition = function(blockType, rootBlock, format, workspace) {
|
||||
blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
|
||||
switch (format) {
|
||||
case 'JSON':
|
||||
var code = BlockFactory.formatJson_(blockType, rootBlock);
|
||||
break;
|
||||
case 'JavaScript':
|
||||
var code = BlockFactory.formatJavaScript_(blockType, rootBlock, workspace);
|
||||
break;
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the language code based on constructs made in Blockly.
|
||||
*/
|
||||
BlockFactory.updateLanguage = function() {
|
||||
var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace);
|
||||
if (!rootBlock) {
|
||||
return;
|
||||
}
|
||||
var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
|
||||
if (!blockType) {
|
||||
blockType = BlockFactory.UNNAMED;
|
||||
}
|
||||
var format = document.getElementById('format').value;
|
||||
var code = BlockFactory.getBlockDefinition(blockType, rootBlock, format,
|
||||
BlockFactory.mainWorkspace);
|
||||
BlockFactory.injectCode(code, 'languagePre');
|
||||
BlockFactory.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the language code as JSON.
|
||||
* @param {string} blockType Name of block.
|
||||
* @param {!Blockly.Block} rootBlock Factory_base block.
|
||||
* @return {string} Generanted language code.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.formatJson_ = function(blockType, rootBlock) {
|
||||
var JS = {};
|
||||
// Type is not used by Blockly, but may be used by a loader.
|
||||
JS.type = blockType;
|
||||
// Generate inputs.
|
||||
var message = [];
|
||||
var args = [];
|
||||
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
|
||||
var lastInput = null;
|
||||
while (contentsBlock) {
|
||||
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
|
||||
var fields = BlockFactory.getFieldsJson_(
|
||||
contentsBlock.getInputTargetBlock('FIELDS'));
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
if (typeof fields[i] == 'string') {
|
||||
message.push(fields[i].replace(/%/g, '%%'));
|
||||
} else {
|
||||
args.push(fields[i]);
|
||||
message.push('%' + args.length);
|
||||
}
|
||||
}
|
||||
|
||||
var input = {type: contentsBlock.type};
|
||||
// Dummy inputs don't have names. Other inputs do.
|
||||
if (contentsBlock.type != 'input_dummy') {
|
||||
input.name = contentsBlock.getFieldValue('INPUTNAME');
|
||||
}
|
||||
var check = JSON.parse(
|
||||
BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE') || 'null');
|
||||
if (check) {
|
||||
input.check = check;
|
||||
}
|
||||
var align = contentsBlock.getFieldValue('ALIGN');
|
||||
if (align != 'LEFT') {
|
||||
input.align = align;
|
||||
}
|
||||
args.push(input);
|
||||
message.push('%' + args.length);
|
||||
lastInput = contentsBlock;
|
||||
}
|
||||
contentsBlock = contentsBlock.nextConnection &&
|
||||
contentsBlock.nextConnection.targetBlock();
|
||||
}
|
||||
// Remove last input if dummy and not empty.
|
||||
if (lastInput && lastInput.type == 'input_dummy') {
|
||||
var fields = lastInput.getInputTargetBlock('FIELDS');
|
||||
if (fields && BlockFactory.getFieldsJson_(fields).join('').trim() != '') {
|
||||
var align = lastInput.getFieldValue('ALIGN');
|
||||
if (align != 'LEFT') {
|
||||
JS.lastDummyAlign0 = align;
|
||||
}
|
||||
args.pop();
|
||||
message.pop();
|
||||
}
|
||||
}
|
||||
JS.message0 = message.join(' ');
|
||||
if (args.length) {
|
||||
JS.args0 = args;
|
||||
}
|
||||
// Generate inline/external switch.
|
||||
if (rootBlock.getFieldValue('INLINE') == 'EXT') {
|
||||
JS.inputsInline = false;
|
||||
} else if (rootBlock.getFieldValue('INLINE') == 'INT') {
|
||||
JS.inputsInline = true;
|
||||
}
|
||||
// Generate output, or next/previous connections.
|
||||
switch (rootBlock.getFieldValue('CONNECTIONS')) {
|
||||
case 'LEFT':
|
||||
JS.output =
|
||||
JSON.parse(
|
||||
BlockFactory.getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
|
||||
break;
|
||||
case 'BOTH':
|
||||
JS.previousStatement =
|
||||
JSON.parse(
|
||||
BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
|
||||
JS.nextStatement =
|
||||
JSON.parse(
|
||||
BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
|
||||
break;
|
||||
case 'TOP':
|
||||
JS.previousStatement =
|
||||
JSON.parse(
|
||||
BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
|
||||
break;
|
||||
case 'BOTTOM':
|
||||
JS.nextStatement =
|
||||
JSON.parse(
|
||||
BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
|
||||
break;
|
||||
}
|
||||
// Generate colour.
|
||||
var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
|
||||
if (colourBlock && !colourBlock.disabled) {
|
||||
var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
|
||||
JS.colour = hue;
|
||||
}
|
||||
JS.tooltip = '';
|
||||
JS.helpUrl = 'http://www.example.com/';
|
||||
return JSON.stringify(JS, null, ' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the language code as JavaScript.
|
||||
* @param {string} blockType Name of block.
|
||||
* @param {!Blockly.Block} rootBlock Factory_base block.
|
||||
* @param {!Blockly.Workspace} workspace - Where the root block lives.
|
||||
|
||||
* @return {string} Generated language code.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.formatJavaScript_ = function(blockType, rootBlock, workspace) {
|
||||
var code = [];
|
||||
code.push("Blockly.Blocks['" + blockType + "'] = {");
|
||||
code.push(" init: function() {");
|
||||
// Generate inputs.
|
||||
var TYPES = {'input_value': 'appendValueInput',
|
||||
'input_statement': 'appendStatementInput',
|
||||
'input_dummy': 'appendDummyInput'};
|
||||
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
|
||||
while (contentsBlock) {
|
||||
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
|
||||
var name = '';
|
||||
// Dummy inputs don't have names. Other inputs do.
|
||||
if (contentsBlock.type != 'input_dummy') {
|
||||
name =
|
||||
BlockFactory.escapeString(contentsBlock.getFieldValue('INPUTNAME'));
|
||||
}
|
||||
code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')');
|
||||
var check = BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE');
|
||||
if (check) {
|
||||
code.push(' .setCheck(' + check + ')');
|
||||
}
|
||||
var align = contentsBlock.getFieldValue('ALIGN');
|
||||
if (align != 'LEFT') {
|
||||
code.push(' .setAlign(Blockly.ALIGN_' + align + ')');
|
||||
}
|
||||
var fields = BlockFactory.getFieldsJs_(
|
||||
contentsBlock.getInputTargetBlock('FIELDS'));
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
code.push(' .appendField(' + fields[i] + ')');
|
||||
}
|
||||
// Add semicolon to last line to finish the statement.
|
||||
code[code.length - 1] += ';';
|
||||
}
|
||||
contentsBlock = contentsBlock.nextConnection &&
|
||||
contentsBlock.nextConnection.targetBlock();
|
||||
}
|
||||
// Generate inline/external switch.
|
||||
if (rootBlock.getFieldValue('INLINE') == 'EXT') {
|
||||
code.push(' this.setInputsInline(false);');
|
||||
} else if (rootBlock.getFieldValue('INLINE') == 'INT') {
|
||||
code.push(' this.setInputsInline(true);');
|
||||
}
|
||||
// Generate output, or next/previous connections.
|
||||
switch (rootBlock.getFieldValue('CONNECTIONS')) {
|
||||
case 'LEFT':
|
||||
code.push(BlockFactory.connectionLineJs_('setOutput', 'OUTPUTTYPE', workspace));
|
||||
break;
|
||||
case 'BOTH':
|
||||
code.push(
|
||||
BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
|
||||
code.push(
|
||||
BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
|
||||
break;
|
||||
case 'TOP':
|
||||
code.push(
|
||||
BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
|
||||
break;
|
||||
case 'BOTTOM':
|
||||
code.push(
|
||||
BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
|
||||
break;
|
||||
}
|
||||
// Generate colour.
|
||||
var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
|
||||
if (colourBlock && !colourBlock.disabled) {
|
||||
var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
|
||||
if (!isNaN(hue)) {
|
||||
code.push(' this.setColour(' + hue + ');');
|
||||
}
|
||||
}
|
||||
code.push(" this.setTooltip('');");
|
||||
code.push(" this.setHelpUrl('http://www.example.com/');");
|
||||
code.push(' }');
|
||||
code.push('};');
|
||||
return code.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create JS code required to create a top, bottom, or value connection.
|
||||
* @param {string} functionName JavaScript function name.
|
||||
* @param {string} typeName Name of type input.
|
||||
* @param {!Blockly.Workspace} workspace - Where the root block lives.
|
||||
* @return {string} Line of JavaScript code to create connection.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.connectionLineJs_ = function(functionName, typeName, workspace) {
|
||||
var type = BlockFactory.getOptTypesFrom(
|
||||
BlockFactory.getRootBlock(workspace), typeName);
|
||||
if (type) {
|
||||
type = ', ' + type;
|
||||
} else {
|
||||
type = '';
|
||||
}
|
||||
return ' this.' + functionName + '(true' + type + ');';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns field strings and any config.
|
||||
* @param {!Blockly.Block} block Input block.
|
||||
* @return {!Array.<string>} Field strings.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.getFieldsJs_ = function(block) {
|
||||
var fields = [];
|
||||
while (block) {
|
||||
if (!block.disabled && !block.getInheritedDisabled()) {
|
||||
switch (block.type) {
|
||||
case 'field_static':
|
||||
// Result: 'hello'
|
||||
fields.push(BlockFactory.escapeString(block.getFieldValue('TEXT')));
|
||||
break;
|
||||
case 'field_input':
|
||||
// Result: new Blockly.FieldTextInput('Hello'), 'GREET'
|
||||
fields.push('new Blockly.FieldTextInput(' +
|
||||
BlockFactory.escapeString(block.getFieldValue('TEXT')) + '), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_number':
|
||||
// Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER'
|
||||
var args = [
|
||||
Number(block.getFieldValue('VALUE')),
|
||||
Number(block.getFieldValue('MIN')),
|
||||
Number(block.getFieldValue('MAX')),
|
||||
Number(block.getFieldValue('PRECISION'))
|
||||
];
|
||||
// Remove any trailing arguments that aren't needed.
|
||||
if (args[3] == 0) {
|
||||
args.pop();
|
||||
if (args[2] == Infinity) {
|
||||
args.pop();
|
||||
if (args[1] == -Infinity) {
|
||||
args.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_angle':
|
||||
// Result: new Blockly.FieldAngle(90), 'ANGLE'
|
||||
fields.push('new Blockly.FieldAngle(' +
|
||||
parseFloat(block.getFieldValue('ANGLE')) + '), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_checkbox':
|
||||
// Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
|
||||
fields.push('new Blockly.FieldCheckbox(' +
|
||||
BlockFactory.escapeString(block.getFieldValue('CHECKED')) +
|
||||
'), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_colour':
|
||||
// Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
|
||||
fields.push('new Blockly.FieldColour(' +
|
||||
BlockFactory.escapeString(block.getFieldValue('COLOUR')) +
|
||||
'), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_date':
|
||||
// Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
|
||||
fields.push('new Blockly.FieldDate(' +
|
||||
BlockFactory.escapeString(block.getFieldValue('DATE')) + '), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_variable':
|
||||
// Result: new Blockly.FieldVariable('item'), 'VAR'
|
||||
var varname
|
||||
= BlockFactory.escapeString(block.getFieldValue('TEXT') || null);
|
||||
fields.push('new Blockly.FieldVariable(' + varname + '), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
break;
|
||||
case 'field_dropdown':
|
||||
// Result:
|
||||
// new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE'
|
||||
var options = [];
|
||||
for (var i = 0; i < block.optionCount_; i++) {
|
||||
options[i] = '[' +
|
||||
BlockFactory.escapeString(block.getFieldValue('USER' + i)) +
|
||||
', ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('CPU' + i)) + ']';
|
||||
}
|
||||
if (options.length) {
|
||||
fields.push('new Blockly.FieldDropdown([' +
|
||||
options.join(', ') + ']), ' +
|
||||
BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
|
||||
}
|
||||
break;
|
||||
case 'field_image':
|
||||
// Result: new Blockly.FieldImage('http://...', 80, 60)
|
||||
var src = BlockFactory.escapeString(block.getFieldValue('SRC'));
|
||||
var width = Number(block.getFieldValue('WIDTH'));
|
||||
var height = Number(block.getFieldValue('HEIGHT'));
|
||||
var alt = BlockFactory.escapeString(block.getFieldValue('ALT'));
|
||||
fields.push('new Blockly.FieldImage(' +
|
||||
src + ', ' + width + ', ' + height + ', ' + alt + ')');
|
||||
break;
|
||||
}
|
||||
}
|
||||
block = block.nextConnection && block.nextConnection.targetBlock();
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns field strings and any config.
|
||||
* @param {!Blockly.Block} block Input block.
|
||||
* @return {!Array.<string|!Object>} Array of static text and field configs.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.getFieldsJson_ = function(block) {
|
||||
var fields = [];
|
||||
while (block) {
|
||||
if (!block.disabled && !block.getInheritedDisabled()) {
|
||||
switch (block.type) {
|
||||
case 'field_static':
|
||||
// Result: 'hello'
|
||||
fields.push(block.getFieldValue('TEXT'));
|
||||
break;
|
||||
case 'field_input':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
text: block.getFieldValue('TEXT')
|
||||
});
|
||||
break;
|
||||
case 'field_number':
|
||||
var obj = {
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
value: parseFloat(block.getFieldValue('VALUE'))
|
||||
};
|
||||
var min = parseFloat(block.getFieldValue('MIN'));
|
||||
if (min > -Infinity) {
|
||||
obj.min = min;
|
||||
}
|
||||
var max = parseFloat(block.getFieldValue('MAX'));
|
||||
if (max < Infinity) {
|
||||
obj.max = max;
|
||||
}
|
||||
var precision = parseFloat(block.getFieldValue('PRECISION'));
|
||||
if (precision) {
|
||||
obj.precision = precision;
|
||||
}
|
||||
fields.push(obj);
|
||||
break;
|
||||
case 'field_angle':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
angle: Number(block.getFieldValue('ANGLE'))
|
||||
});
|
||||
break;
|
||||
case 'field_checkbox':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
checked: block.getFieldValue('CHECKED') == 'TRUE'
|
||||
});
|
||||
break;
|
||||
case 'field_colour':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
colour: block.getFieldValue('COLOUR')
|
||||
});
|
||||
break;
|
||||
case 'field_date':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
date: block.getFieldValue('DATE')
|
||||
});
|
||||
break;
|
||||
case 'field_variable':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
variable: block.getFieldValue('TEXT') || null
|
||||
});
|
||||
break;
|
||||
case 'field_dropdown':
|
||||
var options = [];
|
||||
for (var i = 0; i < block.optionCount_; i++) {
|
||||
options[i] = [block.getFieldValue('USER' + i),
|
||||
block.getFieldValue('CPU' + i)];
|
||||
}
|
||||
if (options.length) {
|
||||
fields.push({
|
||||
type: block.type,
|
||||
name: block.getFieldValue('FIELDNAME'),
|
||||
options: options
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'field_image':
|
||||
fields.push({
|
||||
type: block.type,
|
||||
src: block.getFieldValue('SRC'),
|
||||
width: Number(block.getFieldValue('WIDTH')),
|
||||
height: Number(block.getFieldValue('HEIGHT')),
|
||||
alt: block.getFieldValue('ALT')
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
block = block.nextConnection && block.nextConnection.targetBlock();
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the type(s) defined in the given input.
|
||||
* Format as a string for appending to the generated code.
|
||||
* @param {!Blockly.Block} block Block with input.
|
||||
* @param {string} name Name of the input.
|
||||
* @return {?string} String defining the types.
|
||||
*/
|
||||
BlockFactory.getOptTypesFrom = function(block, name) {
|
||||
var types = BlockFactory.getTypesFrom_(block, name);
|
||||
if (types.length == 0) {
|
||||
return undefined;
|
||||
} else if (types.indexOf('null') != -1) {
|
||||
return 'null';
|
||||
} else if (types.length == 1) {
|
||||
return types[0];
|
||||
} else {
|
||||
return '[' + types.join(', ') + ']';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the type(s) defined in the given input.
|
||||
* @param {!Blockly.Block} block Block with input.
|
||||
* @param {string} name Name of the input.
|
||||
* @return {!Array.<string>} List of types.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.getTypesFrom_ = function(block, name) {
|
||||
var typeBlock = block.getInputTargetBlock(name);
|
||||
var types;
|
||||
if (!typeBlock || typeBlock.disabled) {
|
||||
types = [];
|
||||
} else if (typeBlock.type == 'type_other') {
|
||||
types = [BlockFactory.escapeString(typeBlock.getFieldValue('TYPE'))];
|
||||
} else if (typeBlock.type == 'type_group') {
|
||||
types = [];
|
||||
for (var n = 0; n < typeBlock.typeCount_; n++) {
|
||||
types = types.concat(BlockFactory.getTypesFrom_(typeBlock, 'TYPE' + n));
|
||||
}
|
||||
// Remove duplicates.
|
||||
var hash = Object.create(null);
|
||||
for (var n = types.length - 1; n >= 0; n--) {
|
||||
if (hash[types[n]]) {
|
||||
types.splice(n, 1);
|
||||
}
|
||||
hash[types[n]] = true;
|
||||
}
|
||||
} else {
|
||||
types = [BlockFactory.escapeString(typeBlock.valueType)];
|
||||
}
|
||||
return types;
|
||||
};
|
||||
|
||||
// Generator Code
|
||||
|
||||
/**
|
||||
* Get the generator code for a given block.
|
||||
*
|
||||
* @param {!Blockly.Block} block - Rendered block in preview workspace.
|
||||
* @param {string} generatorLanguage - 'JavaScript', 'Python', 'PHP', 'Lua',
|
||||
* 'Dart'.
|
||||
* @return {string} Generator code for multiple blocks.
|
||||
*/
|
||||
BlockFactory.getGeneratorStub = function(block, generatorLanguage) {
|
||||
function makeVar(root, name) {
|
||||
name = name.toLowerCase().replace(/\W/g, '_');
|
||||
return ' var ' + root + '_' + name;
|
||||
}
|
||||
// The makevar function lives in the original update generator.
|
||||
var language = generatorLanguage;
|
||||
var code = [];
|
||||
code.push("Blockly." + language + "['" + block.type +
|
||||
"'] = function(block) {");
|
||||
|
||||
// Generate getters for any fields or inputs.
|
||||
for (var i = 0, input; input = block.inputList[i]; i++) {
|
||||
for (var j = 0, field; field = input.fieldRow[j]; j++) {
|
||||
var name = field.name;
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
if (field instanceof Blockly.FieldVariable) {
|
||||
// Subclass of Blockly.FieldDropdown, must test first.
|
||||
code.push(makeVar('variable', name) +
|
||||
" = Blockly." + language +
|
||||
".variableDB_.getName(block.getFieldValue('" + name +
|
||||
"'), Blockly.Variables.NAME_TYPE);");
|
||||
} else if (field instanceof Blockly.FieldAngle) {
|
||||
// Subclass of Blockly.FieldTextInput, must test first.
|
||||
code.push(makeVar('angle', name) +
|
||||
" = block.getFieldValue('" + name + "');");
|
||||
} else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) {
|
||||
// Blockly.FieldDate may not be compiled into Blockly.
|
||||
code.push(makeVar('date', name) +
|
||||
" = block.getFieldValue('" + name + "');");
|
||||
} else if (field instanceof Blockly.FieldColour) {
|
||||
code.push(makeVar('colour', name) +
|
||||
" = block.getFieldValue('" + name + "');");
|
||||
} else if (field instanceof Blockly.FieldCheckbox) {
|
||||
code.push(makeVar('checkbox', name) +
|
||||
" = block.getFieldValue('" + name + "') == 'TRUE';");
|
||||
} else if (field instanceof Blockly.FieldDropdown) {
|
||||
code.push(makeVar('dropdown', name) +
|
||||
" = block.getFieldValue('" + name + "');");
|
||||
} else if (field instanceof Blockly.FieldNumber) {
|
||||
code.push(makeVar('number', name) +
|
||||
" = block.getFieldValue('" + name + "');");
|
||||
} else if (field instanceof Blockly.FieldTextInput) {
|
||||
code.push(makeVar('text', name) +
|
||||
" = block.getFieldValue('" + name + "');");
|
||||
}
|
||||
}
|
||||
var name = input.name;
|
||||
if (name) {
|
||||
if (input.type == Blockly.INPUT_VALUE) {
|
||||
code.push(makeVar('value', name) +
|
||||
" = Blockly." + language + ".valueToCode(block, '" + name +
|
||||
"', Blockly." + language + ".ORDER_ATOMIC);");
|
||||
} else if (input.type == Blockly.NEXT_STATEMENT) {
|
||||
code.push(makeVar('statements', name) +
|
||||
" = Blockly." + language + ".statementToCode(block, '" +
|
||||
name + "');");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Most languages end lines with a semicolon. Python does not.
|
||||
var lineEnd = {
|
||||
'JavaScript': ';',
|
||||
'Python': '',
|
||||
'PHP': ';',
|
||||
'Dart': ';'
|
||||
};
|
||||
code.push(" // TODO: Assemble " + language + " into code variable.");
|
||||
if (block.outputConnection) {
|
||||
code.push(" var code = '...';");
|
||||
code.push(" // TODO: Change ORDER_NONE to the correct strength.");
|
||||
code.push(" return [code, Blockly." + language + ".ORDER_NONE];");
|
||||
} else {
|
||||
code.push(" var code = '..." + (lineEnd[language] || '') + "\\n';");
|
||||
code.push(" return code;");
|
||||
}
|
||||
code.push("};");
|
||||
|
||||
return code.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the generator code.
|
||||
* @param {!Blockly.Block} block Rendered block in preview workspace.
|
||||
*/
|
||||
BlockFactory.updateGenerator = function(block) {
|
||||
var language = document.getElementById('language').value;
|
||||
var generatorStub = BlockFactory.getGeneratorStub(block, language);
|
||||
BlockFactory.injectCode(generatorStub, 'generatorPre');
|
||||
};
|
||||
|
||||
// Preview Block
|
||||
|
||||
/**
|
||||
* Update the preview display.
|
||||
*/
|
||||
BlockFactory.updatePreview = function() {
|
||||
// Toggle between LTR/RTL if needed (also used in first display).
|
||||
var newDir = document.getElementById('direction').value;
|
||||
if (BlockFactory.oldDir != newDir) {
|
||||
if (BlockFactory.previewWorkspace) {
|
||||
BlockFactory.previewWorkspace.dispose();
|
||||
}
|
||||
var rtl = newDir == 'rtl';
|
||||
BlockFactory.previewWorkspace = Blockly.inject('preview',
|
||||
{rtl: rtl,
|
||||
media: '../../media/',
|
||||
scrollbars: true});
|
||||
BlockFactory.oldDir = newDir;
|
||||
}
|
||||
BlockFactory.previewWorkspace.clear();
|
||||
|
||||
// Fetch the code and determine its format (JSON or JavaScript).
|
||||
var format = document.getElementById('format').value;
|
||||
if (format == 'Manual') {
|
||||
var code = document.getElementById('languageTA').value;
|
||||
// If the code is JSON, it will parse, otherwise treat as JS.
|
||||
try {
|
||||
JSON.parse(code);
|
||||
format = 'JSON';
|
||||
} catch (e) {
|
||||
format = 'JavaScript';
|
||||
}
|
||||
} else {
|
||||
var code = document.getElementById('languagePre').textContent;
|
||||
}
|
||||
if (!code.trim()) {
|
||||
// Nothing to render. Happens while cloud storage is loading.
|
||||
return;
|
||||
}
|
||||
|
||||
// Backup Blockly.Blocks object so that main workspace and preview don't
|
||||
// collide if user creates a 'factory_base' block, for instance.
|
||||
var backupBlocks = Blockly.Blocks;
|
||||
console.log(backupBlocks);
|
||||
try {
|
||||
// Make a shallow copy.
|
||||
Blockly.Blocks = Object.create(null);
|
||||
for (var prop in backupBlocks) {
|
||||
Blockly.Blocks[prop] = backupBlocks[prop];
|
||||
}
|
||||
|
||||
if (format == 'JSON') {
|
||||
var json = JSON.parse(code);
|
||||
Blockly.Blocks[json.type || BlockFactory.UNNAMED] = {
|
||||
init: function() {
|
||||
this.jsonInit(json);
|
||||
}
|
||||
};
|
||||
} else if (format == 'JavaScript') {
|
||||
eval(code);
|
||||
} else {
|
||||
throw 'Unknown format: ' + format;
|
||||
}
|
||||
|
||||
// Look for a block on Blockly.Blocks that does not match the backup.
|
||||
var blockType = null;
|
||||
console.log('Blockly Blocks types');
|
||||
for (var type in Blockly.Blocks) {
|
||||
console.log(type);
|
||||
if (typeof Blockly.Blocks[type].init == 'function' &&
|
||||
Blockly.Blocks[type] != backupBlocks[type]) {
|
||||
blockType = type;
|
||||
console.log('found non matching type');
|
||||
console.log(blockType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!blockType) {
|
||||
console.log('non matching type NOT FOUND');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the preview block.
|
||||
var previewBlock = BlockFactory.previewWorkspace.newBlock(blockType);
|
||||
previewBlock.initSvg();
|
||||
previewBlock.render();
|
||||
previewBlock.setMovable(false);
|
||||
previewBlock.setDeletable(false);
|
||||
previewBlock.moveBy(15, 10);
|
||||
BlockFactory.previewWorkspace.clearUndo();
|
||||
BlockFactory.updateGenerator(previewBlock);
|
||||
} finally {
|
||||
Blockly.Blocks = backupBlocks;
|
||||
}
|
||||
};
|
||||
|
||||
// File Import, Creation, Download
|
||||
|
||||
/**
|
||||
* Generate a file from the contents of a given text area and
|
||||
* download that file.
|
||||
* @param {string} filename The name of the file to create.
|
||||
* @param {string} id The text area to download.
|
||||
*/
|
||||
BlockFactory.downloadTextArea = function(filename, id) {
|
||||
var code = document.getElementById(id).textContent;
|
||||
BlockFactory.createAndDownloadFile_(code, filename, 'plain');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a file with the given attributes and download it.
|
||||
* @param {string} contents - The contents of the file.
|
||||
* @param {string} filename - The name of the file to save to.
|
||||
* @param {string} fileType - The type of the file to save.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.createAndDownloadFile_ = function(contents, filename, fileType) {
|
||||
var data = new Blob([contents], {type: 'text/' + fileType});
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the workspace's xml representation to a file.
|
||||
* @private
|
||||
*/
|
||||
BlockFactory.saveWorkspaceToFile = function() {
|
||||
var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace);
|
||||
var prettyXml = Blockly.Xml.domToPrettyText(xmlElement);
|
||||
BlockFactory.createAndDownloadFile_(prettyXml, 'blockXml', 'xml');
|
||||
};
|
||||
|
||||
/**
|
||||
* Imports xml file for a block to the workspace.
|
||||
*/
|
||||
BlockFactory.importBlockFromFile = function() {
|
||||
var files = document.getElementById('files');
|
||||
// If the file list is empty, they user likely canceled in the dialog.
|
||||
if (files.files.length > 0) {
|
||||
// The input tag doesn't have the "mulitple" attribute
|
||||
// so the user can only choose 1 file.
|
||||
var file = files.files[0];
|
||||
var fileReader = new FileReader();
|
||||
fileReader.addEventListener('load', function(event) {
|
||||
var fileContents = event.target.result;
|
||||
var xml = '';
|
||||
try {
|
||||
xml = Blockly.Xml.textToDom(fileContents);
|
||||
} catch (e) {
|
||||
var message = 'Could not load your saved file.\n'+
|
||||
'Perhaps it was created with a different version of Blockly?';
|
||||
window.alert(message + '\nXML: ' + fileContents);
|
||||
return;
|
||||
}
|
||||
BlockFactory.mainWorkspace.clear();
|
||||
Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace);
|
||||
});
|
||||
|
||||
fileReader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable link and save buttons if the format is 'Manual', enable otherwise.
|
||||
*/
|
||||
BlockFactory.disableEnableLink = function() {
|
||||
var linkButton = document.getElementById('linkButton');
|
||||
var saveBlockButton = document.getElementById('localSaveButton');
|
||||
var saveToLibButton = document.getElementById('saveToBlockLibraryButton');
|
||||
var disabled = document.getElementById('format').value == 'Manual';
|
||||
linkButton.disabled = disabled;
|
||||
saveBlockButton.disabled = disabled;
|
||||
saveToLibButton.disabled = disabled;
|
||||
};
|
||||
|
||||
// Block Factory Expansion View Utils
|
||||
|
||||
/**
|
||||
* Render starter block (factory_base).
|
||||
*/
|
||||
BlockFactory.showStarterBlock = function() {
|
||||
var xml = '<xml><block type="factory_base" deletable="false" ' +
|
||||
'movable="false"></block></xml>';
|
||||
Blockly.Xml.domToWorkspace(
|
||||
Blockly.Xml.textToDom(xml), BlockFactory.mainWorkspace);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides element so that it's invisible and doesn't take up space.
|
||||
*
|
||||
* @param {string} elementID - ID of element to hide.
|
||||
*/
|
||||
BlockFactory.hide = function(elementID) {
|
||||
document.getElementById(elementID).style.display = 'none';
|
||||
};
|
||||
|
||||
/**
|
||||
* Un-hides an element.
|
||||
*
|
||||
* @param {string} elementID - ID of element to hide.
|
||||
*/
|
||||
BlockFactory.show = function(elementID) {
|
||||
document.getElementById(elementID).style.display = 'block';
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides element so that it's invisible but still takes up space.
|
||||
*
|
||||
* @param {string} elementID - ID of element to hide.
|
||||
*/
|
||||
BlockFactory.makeInvisible = function(elementID) {
|
||||
document.getElementById(elementID).visibility = 'hidden';
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes element visible.
|
||||
*
|
||||
* @param {string} elementID - ID of element to hide.
|
||||
*/
|
||||
BlockFactory.makeVisible = function(elementID) {
|
||||
document.getElementById(elementID).visibility = 'visible';
|
||||
};
|
||||
|
||||
282
demos/blocklyfactory/index.html
Normal file
282
demos/blocklyfactory/index.html
Normal file
@@ -0,0 +1,282 @@
|
||||
<!-- TODO(quacht): move the CSS out to a separate file -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="target-densitydpi=device-dpi, height=660, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Blockly Demo: Blockly Factory</title>
|
||||
<script src="/storage.js"></script>
|
||||
<script src="../../blockly_uncompressed.js"></script>
|
||||
<script src="../../generators/javascript.js"></script>
|
||||
<script src="../../../closure-library/closure/goog/base.js"></script>
|
||||
<script src="factory.js"></script>
|
||||
<script src="block_library_view.js"></script>
|
||||
<script src="block_library_storage.js"></script>
|
||||
<script src="block_library_controller.js"></script>
|
||||
<script src="block_exporter_tools.js"></script>
|
||||
<script src="block_exporter_view.js"></script>
|
||||
<script src="block_exporter_controller.js"></script>
|
||||
<script src="../blockfactory/blocks.js"></script>
|
||||
<script src="app_controller.js"></script>
|
||||
<link rel="stylesheet" href="factory.css">
|
||||
<link rel="stylesheet" href="../prettify.css">
|
||||
<script src="../prettify.js"></script>
|
||||
<script>
|
||||
var blocklyFactory;
|
||||
var init = function() {
|
||||
blocklyFactory = new AppController();
|
||||
blocklyFactory.init();
|
||||
if (blocklyFactory.blockLibraryController.hasEmptyBlockLib()) {
|
||||
alert('Your block library is empty! Click "Save to Block Library" so ' +
|
||||
'you can reopen it the next time you visit Block Factory!');
|
||||
}
|
||||
};
|
||||
window.addEventListener('load', init);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="https://developers.google.com/blockly/">Blockly</a> >
|
||||
<a href="../index.html">Demos</a> > Blockly Factory</h1>
|
||||
|
||||
<div id="tabContainer">
|
||||
<div id="blockFactory_tab" class="tab tabon"> Block Factory</div>
|
||||
<div class="tabGap"></div>
|
||||
<div id="workspaceFactory_tab" class="tab taboff"> Workspace Factory</div>
|
||||
<div class="tabGap"></div>
|
||||
<div id="blocklibraryExporter_tab" class="tab taboff"> Exporter</div>
|
||||
</div>
|
||||
|
||||
<div id="blockLibraryExporter">
|
||||
<h3> Block Selector </h3>
|
||||
<div id="helperTextDiv">
|
||||
<p id="helperText"> Drag blocks into your workspace to select them for download.</p>
|
||||
</div>
|
||||
<div id="exporterButtons">
|
||||
<button id="clearSelectedButton"> Clear Blocks </button>
|
||||
<button id="addAllFromLibButton"> Add All Stored Blocks </button>
|
||||
<button id="addAllUsedButton"> Add All Used Blocks </button>
|
||||
</div>
|
||||
<!-- Inject exportSelectorWorkspace into this div -->
|
||||
<div id="exportSelector"></div>
|
||||
<!-- Users may customize export settings through this form -->
|
||||
<div id="exportSettings">
|
||||
<h3> Export Settings </h3>
|
||||
<br>
|
||||
<form id="exportSettingsForm">
|
||||
Toolbox Xml:
|
||||
<input type="checkbox" id="toolboxCheck">
|
||||
<br>
|
||||
Pre-loaded Workspace:
|
||||
<input type="checkbox" id="preloadedWorkspaceCheck">
|
||||
<br>
|
||||
Workspace Option(s):
|
||||
<input type="checkbox" id="workspaceOptsCheck">
|
||||
<br>
|
||||
<br>
|
||||
Block Definition(s):
|
||||
<input type="checkbox" id="blockDefCheck"><br>
|
||||
Language code:
|
||||
<select id="exportFormat">
|
||||
<option value="JSON">JSON</option>
|
||||
<option value="JavaScript">JavaScript</option>
|
||||
</select><br>
|
||||
Block Definition(s) File Name:<br>
|
||||
<input type="text" id="blockDef_filename">
|
||||
<br>
|
||||
<br>
|
||||
Generator Stub(s):
|
||||
<input type="checkbox" id="genStubCheck"><br>
|
||||
<select id="exportLanguage">
|
||||
<option value="JavaScript">JavaScript</option>
|
||||
<option value="Python">Python</option>
|
||||
<option value="PHP">PHP</option>
|
||||
<option value="Lua">Lua</option>
|
||||
<option value="Dart">Dart</option>
|
||||
</select><br>
|
||||
Block Generator Stub(s) File Name: <br>
|
||||
<input type="text" id="generatorStub_filename"><br>
|
||||
<br>
|
||||
</form>
|
||||
<button id="exporterSubmitButton"> Export </button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workspaceFactoryContent">
|
||||
</div>
|
||||
|
||||
<table id="blockFactoryContent">
|
||||
<tr>
|
||||
<td width="50%" height="5%">
|
||||
<table>
|
||||
<tr id="blockLibrary">
|
||||
<td id="blockLibraryContainer">
|
||||
<span>
|
||||
<h3>Block Library:</h3>
|
||||
<select id="blockLibraryDropdown">
|
||||
</select>
|
||||
</span>
|
||||
</td>
|
||||
<td id="blockLibraryControls">
|
||||
<button id="saveToBlockLibraryButton" title="Save block to Block Library.">
|
||||
<span>Save Block</span>
|
||||
</button>
|
||||
<button id="removeBlockFromLibraryButton" title="Remove block from Block Library.">
|
||||
<span>Delete Block</span>
|
||||
</button>
|
||||
<button id="clearBlockLibraryButton" title="Clear Block Library.">
|
||||
<span>Clear Library</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
<table>
|
||||
<tr>
|
||||
<td id="previewContainer">
|
||||
<h3>Preview:
|
||||
<select id="direction">
|
||||
<option value="ltr">LTR</option>
|
||||
<option value="rtl">RTL</option>
|
||||
</select>
|
||||
</h3>
|
||||
</td>
|
||||
<td id="buttonContainer">
|
||||
<button id="linkButton" title="Save and link to blocks.">
|
||||
<img src="link.png" height="21" width="21">
|
||||
</button>
|
||||
<button id="linkButton" title="Save and link to blocks.">
|
||||
<img src="link.png" height="21" width="21">
|
||||
</button>
|
||||
<button id="createNewBlockButton" title="Create a new block.">
|
||||
<span> Create New Block</span>
|
||||
</button>
|
||||
<label for="files" class="buttonStyle">
|
||||
<span class=>Import Block Library</span>
|
||||
</label>
|
||||
<input id="files" type="file" name="files"
|
||||
accept="application/xml">
|
||||
<button id="localSaveButton" title="Save block library xml to a local file.">
|
||||
<span>Download Block Library</span>
|
||||
</button>
|
||||
<button id="helpButton" title="View documentation in new window.">
|
||||
<span>Help</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="blocklyWorkspaceContainer">
|
||||
<div id="blockly"></div>
|
||||
<div id="blocklyMask"></div>
|
||||
</td>
|
||||
<td width="50%" height="95%">
|
||||
<table>
|
||||
<tr>
|
||||
<td height="30%">
|
||||
<div id="preview"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="5%">
|
||||
<h3>Language code:
|
||||
<select id="format">
|
||||
<option value="JSON">JSON</option>
|
||||
<option value="JavaScript">JavaScript</option>
|
||||
<option value="Manual">Manual edit…</option>
|
||||
</select>
|
||||
<button class ="downloadButton" id="downloadBlocks"
|
||||
title="Download block definition to a file.">
|
||||
<span>Download</span>
|
||||
</button>
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="30%">
|
||||
<pre id="languagePre"></pre>
|
||||
<textarea id="languageTA"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="5%">
|
||||
<h3>Generator stub:
|
||||
<select id="language">
|
||||
<option value="JavaScript">JavaScript</option>
|
||||
<option value="Python">Python</option>
|
||||
<option value="PHP">PHP</option>
|
||||
<option value="Lua">Lua</option>
|
||||
<option value="Dart">Dart</option>
|
||||
</select>
|
||||
<button id="downloadGenerator" class="downloadButton"
|
||||
title="Downloadgenerator stub to a file.">
|
||||
<span>Download</span>
|
||||
</button>
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td height="30%">
|
||||
<pre id="generatorPre"></pre>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</table>
|
||||
|
||||
<xml id="toolbox">
|
||||
<category name="Input">
|
||||
<block type="input_value">
|
||||
<value name="TYPE">
|
||||
<shadow type="type_null"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="input_statement">
|
||||
<value name="TYPE">
|
||||
<shadow type="type_null"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="input_dummy"></block>
|
||||
</category>
|
||||
<category name="Field">
|
||||
<block type="field_static"></block>
|
||||
<block type="field_input"></block>
|
||||
<block type="field_number"></block>
|
||||
<block type="field_angle"></block>
|
||||
<block type="field_dropdown"></block>
|
||||
<block type="field_checkbox"></block>
|
||||
<block type="field_colour"></block>
|
||||
<!--
|
||||
Date picker commented out since it increases footprint by 60%.
|
||||
Add it only if you need it. See also goog.require in blockly.js.
|
||||
<block type="field_date"></block>
|
||||
-->
|
||||
<block type="field_variable"></block>
|
||||
<block type="field_image"></block>
|
||||
</category>
|
||||
<category name="Type">
|
||||
<block type="type_group"></block>
|
||||
<block type="type_null"></block>
|
||||
<block type="type_boolean"></block>
|
||||
<block type="type_number"></block>
|
||||
<block type="type_string"></block>
|
||||
<block type="type_list"></block>
|
||||
<block type="type_other"></block>
|
||||
</category>
|
||||
<category name="Colour" id="colourCategory">
|
||||
<block type="colour_hue"><mutation colour="20"></mutation><field name="HUE">20</field></block>
|
||||
<block type="colour_hue"><mutation colour="65"></mutation><field name="HUE">65</field></block>
|
||||
<block type="colour_hue"><mutation colour="120"></mutation><field name="HUE">120</field></block>
|
||||
<block type="colour_hue"><mutation colour="160"></mutation><field name="HUE">160</field></block>
|
||||
<block type="colour_hue"><mutation colour="210"></mutation><field name="HUE">210</field></block>
|
||||
<block type="colour_hue"><mutation colour="230"></mutation><field name="HUE">230</field></block>
|
||||
<block type="colour_hue"><mutation colour="260"></mutation><field name="HUE">260</field></block>
|
||||
<block type="colour_hue"><mutation colour="290"></mutation><field name="HUE">290</field></block>
|
||||
<block type="colour_hue"><mutation colour="330"></mutation><field name="HUE">330</field></block>
|
||||
</category>
|
||||
</xml>
|
||||
</body>
|
||||
</html>
|
||||
BIN
demos/blocklyfactory/link.png
Normal file
BIN
demos/blocklyfactory/link.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 228 B |
682
demos/blocklyfactory/workspacefactory/index.html
Normal file
682
demos/blocklyfactory/workspacefactory/index.html
Normal file
@@ -0,0 +1,682 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<title>Blockly Workspace Factory</title>
|
||||
<script src="../../../blockly_compressed.js"></script>
|
||||
<script src="../../../javascript_compressed.js"></script>
|
||||
<script src="../../../msg/messages.js"></script>
|
||||
<script src="../../../blocks_compressed.js"></script>
|
||||
<script src="wfactory_model.js"></script>
|
||||
<script src="wfactory_controller.js"></script>
|
||||
<script src="wfactory_view.js"></script>
|
||||
<script src="wfactory_generator.js"></script>
|
||||
<script src="standard_categories.js"></script>
|
||||
<script src="../../../../closure-library/closure/goog/base.js"></script>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<script>
|
||||
goog.require('goog.ui.PopupColorPicker');
|
||||
goog.require('goog.ui.ColorPicker');
|
||||
</script>
|
||||
|
||||
<table width="100%" height="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<h1><a href="https://developers.google.com/blockly/">Blockly</a>‏ >
|
||||
<a href="../index.html">Demos</a>‏ >
|
||||
<span id="title">Workspace Factory</span>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
<input type="file" id="input_import" class="inputfile"></input>
|
||||
<label for="input_import">Import</label>
|
||||
<button id="button_export">Export</button>
|
||||
<button id="button_print">Print</button>
|
||||
<button id="button_clear">Clear</button>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<section id="createDiv">
|
||||
<p>Drag blocks into your toolbox:</p>
|
||||
<section id="toolbox_section">
|
||||
<div id="toolbox_blocks" class="content"></div>
|
||||
<div id='disable_div'></div>
|
||||
</section>
|
||||
<aside id="category_section">
|
||||
<table id="categoryTable">
|
||||
<td id="tab_help">Your categories will appear here</td>
|
||||
</table>
|
||||
<p> </p>
|
||||
|
||||
<div class='dropdown'>
|
||||
<button id="button_add">+</button>
|
||||
<div id="dropdownDiv_add" class="dropdown-content">
|
||||
<a id='dropdown_newCategory'>New Category</a>
|
||||
<a id='dropdown_loadCategory'>Standard Category</a>
|
||||
<a id='dropdown_separator'>Separator</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="button_remove">-</button>
|
||||
|
||||
<button id="button_up">↑</button>
|
||||
<button id="button_down">↓</button>
|
||||
|
||||
<p> </p>
|
||||
<div class='dropdown'>
|
||||
<button id="button_editCategory">Edit Category</button>
|
||||
<div id="dropdownDiv_editCategory" class="dropdown-content">
|
||||
<a id='dropdown_name'>Name</a>
|
||||
<a id='dropdown_color'>Color</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='dropdown'>
|
||||
<button id="button_editShadow">Edit Block</button>
|
||||
<div id="dropdownDiv_editShadowAdd" class="dropdown-content">
|
||||
<a id='dropdown_addShadow'>Add Shadow</a>
|
||||
</div>
|
||||
<div id="dropdownDiv_editShadowRemove" class="dropdown-content">
|
||||
<a id='dropdown_removeShadow'>Remove Shadow</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<aside id="previewDiv">
|
||||
<p>Preview your workspace:</p>
|
||||
<div id="preview_blocks" class="content"></div>
|
||||
</aside>
|
||||
|
||||
<xml id="toolbox" style="display: none">
|
||||
<category name="Logic" colour="210">
|
||||
<block type="controls_if"></block>
|
||||
<block type="logic_compare"></block>
|
||||
<block type="logic_operation"></block>
|
||||
<block type="logic_negate"></block>
|
||||
<block type="logic_boolean"></block>
|
||||
<block type="logic_null"></block>
|
||||
<block type="logic_ternary"></block>
|
||||
</category>
|
||||
<category name="Loops" colour="120">
|
||||
<block type="controls_repeat_ext">
|
||||
<value name="TIMES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_whileUntil"></block>
|
||||
<block type="controls_for">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="BY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="controls_forEach"></block>
|
||||
<block type="controls_flow_statements"></block>
|
||||
</category>
|
||||
<category name="Math" colour="230">
|
||||
<block type="math_number"></block>
|
||||
<block type="math_arithmetic">
|
||||
<value name="A">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="B">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_single">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">9</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_trig">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">45</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_constant"></block>
|
||||
<block type="math_number_property">
|
||||
<value name="NUMBER_TO_CHECK">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_change">
|
||||
<value name="DELTA">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_round">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">3.1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_on_list"></block>
|
||||
<block type="math_modulo">
|
||||
<value name="DIVIDEND">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">64</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="DIVISOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_constrain">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="LOW">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="HIGH">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_int">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="math_random_float"></block>
|
||||
</category>
|
||||
<category name="Text" colour="160">
|
||||
<block type="text"></block>
|
||||
<block type="text_join"></block>
|
||||
<block type="text_append">
|
||||
<value name="TEXT">
|
||||
<shadow type="text"></shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_length">
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_isEmpty">
|
||||
<value name="VALUE">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"></field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_indexOf">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
<value name="FIND">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_charAt">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_getSubstring">
|
||||
<value name="STRING">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">text</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_changeCase">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_trim">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_print">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="text_prompt_ext">
|
||||
<value name="TEXT">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">abc</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
<category name="Lists" colour="260">
|
||||
<block type="lists_create_with">
|
||||
<mutation items="0"></mutation>
|
||||
</block>
|
||||
<block type="lists_create_with"></block>
|
||||
<block type="lists_repeat">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_length"></block>
|
||||
<block type="lists_isEmpty"></block>
|
||||
<block type="lists_indexOf">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_getIndex">
|
||||
<value name="VALUE">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_setIndex">
|
||||
<value name="LIST">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_getSublist">
|
||||
<value name="LIST">
|
||||
<block type="variables_get">
|
||||
<field name="VAR">list</field>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_split">
|
||||
<value name="DELIM">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">,</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="lists_sort"></block>
|
||||
</category>
|
||||
<category name="Colour" colour="20">
|
||||
<block type="colour_picker"></block>
|
||||
<block type="colour_random"></block>
|
||||
<block type="colour_rgb">
|
||||
<value name="RED">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="GREEN">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="BLUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="colour_blend">
|
||||
<value name="COLOUR1">
|
||||
<shadow type="colour_picker">
|
||||
<field name="COLOUR">#ff0000</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="COLOUR2">
|
||||
<shadow type="colour_picker">
|
||||
<field name="COLOUR">#3333ff</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="RATIO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
<sep></sep>
|
||||
<category name="Variables" colour="330" custom="VARIABLE"></category>
|
||||
<category name="Functions" colour="290" custom="PROCEDURE"></category>
|
||||
</xml>
|
||||
<script type="text/javascript">
|
||||
// Array of Blockly category colors, variety of hues with saturation 45%
|
||||
// and value 65% as specified in Blockly Developer documentation:
|
||||
// https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks
|
||||
var colors = ['#A65C5C',
|
||||
'#A6635C',
|
||||
'#A66A5C',
|
||||
'#A6725C',
|
||||
'#A6795C',
|
||||
'#A6815C',
|
||||
'#A6885C',
|
||||
'#A6905C',
|
||||
'#A6975C',
|
||||
'#A69F5C',
|
||||
'#A6A65C',
|
||||
'#9FA65C',
|
||||
'#97A65C',
|
||||
'#90A65C',
|
||||
'#88A65C',
|
||||
'#81A65C',
|
||||
'#79A65C',
|
||||
'#6FA65C',
|
||||
'#66A65C',
|
||||
'#5EA65C',
|
||||
'#5CA661',
|
||||
'#5CA668',
|
||||
'#5CA66F',
|
||||
'#5CA677',
|
||||
'#5CA67E',
|
||||
'#5CA686',
|
||||
'#5CA68D',
|
||||
'#5CA695',
|
||||
'#5CA69C',
|
||||
'#5CA6A4',
|
||||
'#5CA1A6',
|
||||
'#5C9AA6',
|
||||
'#5C92A6',
|
||||
'#5C8BA6',
|
||||
'#5C83A6',
|
||||
'#5C7CA6',
|
||||
'#5C74A6',
|
||||
'#5C6AA6',
|
||||
'#5C61A6',
|
||||
'#5E5CA6',
|
||||
'#665CA6',
|
||||
'#6D5CA6',
|
||||
'#745CA6',
|
||||
'#7C5CA6',
|
||||
'#835CA6',
|
||||
'#8B5CA6',
|
||||
'#925CA6',
|
||||
'#9A5CA6',
|
||||
'#A15CA6',
|
||||
'#A65CA4',
|
||||
'#A65C9C',
|
||||
'#A65C95',
|
||||
'#A65C8D',
|
||||
'#A65C86',
|
||||
'#A65C7E',
|
||||
'#A65C77',
|
||||
'#A65C6F',
|
||||
'#A65C66',
|
||||
'#A65C61',
|
||||
'#A65C5E'];
|
||||
|
||||
// Create empty workspace for configuring workspace.
|
||||
var toolboxWorkspace = Blockly.inject('toolbox_blocks',
|
||||
{grid:
|
||||
{spacing: 25,
|
||||
length: 3,
|
||||
colour: '#ccc',
|
||||
snap: true},
|
||||
media: '../../../media/',
|
||||
toolbox: toolbox,
|
||||
});
|
||||
// Create empty workspace for previewing created workspace.
|
||||
var previewWorkspace = Blockly.inject('preview_blocks',
|
||||
{grid:
|
||||
{spacing: 25,
|
||||
length: 3,
|
||||
colour: '#ccc',
|
||||
snap: true},
|
||||
media: '../../../media/',
|
||||
toolbox: '<xml></xml>',
|
||||
zoom:
|
||||
{controls: true,
|
||||
wheel: true}
|
||||
});
|
||||
|
||||
var controller = new FactoryController(toolboxWorkspace, previewWorkspace);
|
||||
|
||||
// Wrappers to attach buttons to method calls for the controller object
|
||||
var addWrapper = function() {
|
||||
document.getElementById('dropdownDiv_add').classList.toggle("show");
|
||||
};
|
||||
var newCategoryWrapper = function() {
|
||||
controller.addCategory();
|
||||
document.getElementById('dropdownDiv_add').classList.remove("show");
|
||||
};
|
||||
var loadCategoryWrapper = function() {
|
||||
controller.loadCategory();
|
||||
document.getElementById('dropdownDiv_add').classList.remove("show");
|
||||
};
|
||||
var separatorWrapper = function() {
|
||||
controller.addSeparator();
|
||||
document.getElementById('dropdownDiv_add').classList.remove("show");
|
||||
};
|
||||
var removeWrapper = function() {
|
||||
controller.removeElement();
|
||||
};
|
||||
var exportWrapper = function() {
|
||||
controller.exportConfig();
|
||||
};
|
||||
var printWrapper = function() {
|
||||
controller.printConfig();
|
||||
};
|
||||
var upWrapper = function() {
|
||||
controller.moveElement(-1);
|
||||
};
|
||||
var downWrapper = function() {
|
||||
controller.moveElement(1);
|
||||
};
|
||||
var editCategoryWrapper = function() {
|
||||
document.getElementById('dropdownDiv_editCategory').classList.toggle("show");
|
||||
};
|
||||
var nameWrapper = function() {
|
||||
controller.changeCategoryName();
|
||||
document.getElementById('dropdownDiv_editCategory').classList.remove("show");
|
||||
};
|
||||
var shadowAddWrapper = function() {
|
||||
controller.addShadow();
|
||||
document.getElementById('dropdownDiv_editShadowAdd').classList.remove("show");
|
||||
};
|
||||
var shadowRemoveWrapper = function() {
|
||||
controller.removeShadow();
|
||||
document.getElementById('dropdownDiv_editShadowRemove').classList.remove("show");
|
||||
// If turning invalid shadow block back to normal block, remove warning and disable
|
||||
// block editing privileges.
|
||||
Blockly.selected.setWarningText(null);
|
||||
if (!Blockly.selected.getSurroundParent()) {
|
||||
document.getElementById('button_editShadow').disabled = true;
|
||||
}
|
||||
};
|
||||
var editShadowWrapper = function() {
|
||||
if (Blockly.selected) {
|
||||
// Can only edit blocks when a block is selected.
|
||||
|
||||
if (!controller.isUserGenShadowBlock(Blockly.selected.id) && Blockly.selected.getSurroundParent() != null) {
|
||||
// If a block is selected that could be a valid shadow block (not a shadow block,
|
||||
// has a surrounding parent), let the user make it a shadow block.
|
||||
// Use toggle instead of add so that the user can click the button again
|
||||
// to make the dropdown disappear without clicking one of the options.
|
||||
document.getElementById('dropdownDiv_editShadowRemove').classList.remove("show");
|
||||
document.getElementById('dropdownDiv_editShadowAdd').classList.toggle("show");
|
||||
} else {
|
||||
// If the block is a shadow block, let the user make it a normal block.
|
||||
document.getElementById('dropdownDiv_editShadowAdd').classList.remove("show");
|
||||
document.getElementById('dropdownDiv_editShadowRemove').classList.toggle("show");
|
||||
}
|
||||
}
|
||||
};
|
||||
var importWrapper = function(event) {
|
||||
controller.importFile(event.target.files[0]);
|
||||
};
|
||||
var clearWrapper = function() {
|
||||
controller.clear();
|
||||
};
|
||||
|
||||
document.getElementById('button_add').addEventListener
|
||||
('click', addWrapper);
|
||||
document.getElementById('dropdown_newCategory').addEventListener
|
||||
('click', newCategoryWrapper);
|
||||
document.getElementById('dropdown_loadCategory').addEventListener
|
||||
('click', loadCategoryWrapper);
|
||||
document.getElementById('dropdown_separator').addEventListener
|
||||
('click', separatorWrapper);
|
||||
document.getElementById('button_remove').addEventListener
|
||||
('click', removeWrapper);
|
||||
document.getElementById('button_export').addEventListener
|
||||
('click', exportWrapper);
|
||||
document.getElementById('button_print').addEventListener
|
||||
('click', printWrapper);
|
||||
document.getElementById('button_up').addEventListener
|
||||
('click', upWrapper);
|
||||
document.getElementById('button_down').addEventListener
|
||||
('click', downWrapper);
|
||||
document.getElementById('button_editCategory').addEventListener
|
||||
('click', editCategoryWrapper);
|
||||
document.getElementById('button_editShadow').addEventListener
|
||||
('click', editShadowWrapper);
|
||||
document.getElementById('dropdown_name').addEventListener
|
||||
('click', nameWrapper);
|
||||
document.getElementById('input_import').addEventListener
|
||||
('change', importWrapper);
|
||||
document.getElementById('button_clear').addEventListener
|
||||
('click', clearWrapper);
|
||||
document.getElementById('dropdown_addShadow').addEventListener
|
||||
('click', shadowAddWrapper);
|
||||
document.getElementById('dropdown_removeShadow').addEventListener
|
||||
('click', shadowRemoveWrapper);
|
||||
|
||||
// Use up and down arrow keys to move categories.
|
||||
// TODO(evd2014): When merge with next CL for editing preloaded blocks, make sure
|
||||
// mode is toolbox.
|
||||
window.addEventListener('keydown', function(e) {
|
||||
if (e.keyCode == 38) { // Arrow up.
|
||||
upWrapper();
|
||||
} else if (e.keyCode == 40) { // Arrow down.
|
||||
downWrapper();
|
||||
}
|
||||
});
|
||||
|
||||
// Create color picker with specific set of Blockly colors.
|
||||
var colorPicker = new goog.ui.ColorPicker();
|
||||
colorPicker.setColors(colors);
|
||||
// Create and render the popup color picker and attach to button.
|
||||
var popupPicker = new goog.ui.PopupColorPicker(null, colorPicker);
|
||||
popupPicker.render();
|
||||
popupPicker.attach(document.getElementById('dropdown_color'));
|
||||
popupPicker.setFocusable(true);
|
||||
goog.events.listen(popupPicker, 'change', function(e) {
|
||||
controller.changeSelectedCategoryColor(popupPicker.getSelectedColor());
|
||||
document.getElementById('dropdownDiv_editCategory').classList.remove
|
||||
("show");
|
||||
});
|
||||
|
||||
// Disable category editing buttons until categories are created.
|
||||
document.getElementById('button_remove').disabled = true;
|
||||
document.getElementById('button_up').disabled = true;
|
||||
document.getElementById('button_down').disabled = true;
|
||||
document.getElementById('button_editCategory').disabled = true;
|
||||
document.getElementById('button_editShadow').disabled = true;
|
||||
|
||||
toolboxWorkspace.addChangeListener(function(e) {
|
||||
// Listen for Blockly move and delete events to update preview.
|
||||
// Not listening for Blockly create events because causes the user to drop
|
||||
// blocks when dragging them into workspace. Could cause problems if ever load
|
||||
// blocks into workspace directly without calling updatePreview.
|
||||
if (e.type == Blockly.Events.MOVE || e.type == Blockly.Events.DELETE) {
|
||||
controller.updatePreview();
|
||||
}
|
||||
|
||||
// Listen for Blockly UI events to correctly enable the "Edit Block" button.
|
||||
// Only enable "Edit Block" when a block is selected and it has a surrounding
|
||||
// parent, meaning it is nested in another block (blocks that are not
|
||||
// nested in parents cannot be shadow blocks).
|
||||
if (e.type == Blockly.Events.MOVE || (e.type == Blockly.Events.UI &&
|
||||
e.element == 'selected')) {
|
||||
var selected = Blockly.selected;
|
||||
|
||||
if (selected != null && selected.getSurroundParent() != null) {
|
||||
|
||||
// A valid shadow block is selected. Enable block editing and remove warnings.
|
||||
document.getElementById('button_editShadow').disabled = false;
|
||||
Blockly.selected.setWarningText(null);
|
||||
} else {
|
||||
if (selected != null && controller.isUserGenShadowBlock(selected.id)) {
|
||||
|
||||
// Provide warning if shadow block is moved and is no longer a valid shadow block.
|
||||
Blockly.selected.setWarningText('Shadow blocks must be nested inside other' +
|
||||
' blocks to be displayed.');
|
||||
|
||||
// Give editing options so that the user can make an invalid shadow block
|
||||
// a normal block.
|
||||
document.getElementById('button_editShadow').disabled = false;
|
||||
} else {
|
||||
|
||||
// No block selected that is a shadow block or could be a valid shadow block.
|
||||
// Disable block editing.
|
||||
document.getElementById('button_editShadow').disabled = true;
|
||||
document.getElementById('dropdownDiv_editShadowRemove').classList.remove("show");
|
||||
document.getElementById('dropdownDiv_editShadowAdd').classList.remove("show");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert actual shadow blocks added from the toolbox to user-generated shadow blocks.
|
||||
if (e.type == Blockly.Events.CREATE) {
|
||||
controller.convertShadowBlocks();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
375
demos/blocklyfactory/workspacefactory/standard_categories.js
Normal file
375
demos/blocklyfactory/workspacefactory/standard_categories.js
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Contains a map of standard Blockly categories used to load
|
||||
* standard Blockly categories into the user's toolbox. The map is keyed by
|
||||
* the lower case name of the category, and contains the Category object for
|
||||
* that particular category.
|
||||
*
|
||||
* @author Emma Dauterman (evd2014)
|
||||
*/
|
||||
|
||||
FactoryController.prototype.standardCategories = Object.create(null);
|
||||
|
||||
FactoryController.prototype.standardCategories['logic'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Logic');
|
||||
FactoryController.prototype.standardCategories['logic'].xml =
|
||||
Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="controls_if"></block>' +
|
||||
'<block type="logic_compare"></block>' +
|
||||
'<block type="logic_operation"></block>' +
|
||||
'<block type="logic_negate"></block>' +
|
||||
'<block type="logic_boolean"></block>' +
|
||||
'<block type="logic_null"></block>' +
|
||||
'<block type="logic_ternary"></block>' +
|
||||
'</xml>');
|
||||
FactoryController.prototype.standardCategories['logic'].color = '#5C81A6';
|
||||
|
||||
FactoryController.prototype.standardCategories['loops'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Loops');
|
||||
FactoryController.prototype.standardCategories['loops'].xml =
|
||||
Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="controls_repeat_ext">' +
|
||||
'<value name="TIMES">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">10</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="controls_whileUntil"></block>' +
|
||||
'<block type="controls_for">' +
|
||||
'<value name="FROM">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="TO">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">10</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="BY">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="controls_forEach"></block>' +
|
||||
'<block type="controls_flow_statements"></block>' +
|
||||
'</xml>');
|
||||
FactoryController.prototype.standardCategories['loops'].color = '#5CA65C';
|
||||
|
||||
FactoryController.prototype.standardCategories['math'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Math');
|
||||
FactoryController.prototype.standardCategories['math'].xml =
|
||||
Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="math_number"></block>' +
|
||||
'<block type="math_arithmetic">' +
|
||||
'<value name="A">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="B">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_single">' +
|
||||
'<value name="NUM">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">9</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_trig">' +
|
||||
'<value name="NUM">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">45</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_constant"></block>' +
|
||||
'<block type="math_number_property">' +
|
||||
'<value name="NUMBER_TO_CHECK">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">0</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_change">' +
|
||||
'<value name="DELTA">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_round">' +
|
||||
'<value name="NUM">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">3.1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_on_list"></block>' +
|
||||
'<block type="math_modulo">' +
|
||||
'<value name="DIVIDEND">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">64</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="DIVISOR">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">10</field>'+
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_constrain">' +
|
||||
'<value name="VALUE">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">50</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="LOW">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="HIGH">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">100</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_random_int">' +
|
||||
'<value name="FROM">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">1</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="TO">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">100</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="math_random_float"></block>' +
|
||||
'</xml>');
|
||||
FactoryController.prototype.standardCategories['math'].color = '#5C68A6';
|
||||
|
||||
FactoryController.prototype.standardCategories['text'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Text');
|
||||
FactoryController.prototype.standardCategories['text'].xml =
|
||||
Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="text"></block>' +
|
||||
'<block type="text_join"></block>' +
|
||||
'<block type="text_append">' +
|
||||
'<value name="TEXT">' +
|
||||
'<shadow type="text"></shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_length">' +
|
||||
'<value name="VALUE">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">abc</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_isEmpty">' +
|
||||
'<value name="VALUE">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT"></field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_indexOf">' +
|
||||
'<value name="VALUE">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">text</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'<value name="FIND">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">abc</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_charAt">' +
|
||||
'<value name="VALUE">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">text</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_getSubstring">' +
|
||||
'<value name="STRING">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">text</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_changeCase">' +
|
||||
'<value name="TEXT">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">abc</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_trim">' +
|
||||
'<value name="TEXT">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">abc</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_print">' +
|
||||
'<value name="TEXT">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">abc</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="text_prompt_ext">' +
|
||||
'<value name="TEXT">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">abc</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'</xml>');
|
||||
FactoryController.prototype.standardCategories['text'].color = '#5CA68D';
|
||||
|
||||
FactoryController.prototype.standardCategories['lists'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Lists');
|
||||
FactoryController.prototype.standardCategories['lists'].xml =
|
||||
Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="lists_create_with">' +
|
||||
'<mutation items="0"></mutation>' +
|
||||
'</block>' +
|
||||
'<block type="lists_create_with"></block>' +
|
||||
'<block type="lists_repeat">' +
|
||||
'<value name="NUM">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">5</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="lists_length"></block>' +
|
||||
'<block type="lists_isEmpty"></block>' +
|
||||
'<block type="lists_indexOf">' +
|
||||
'<value name="VALUE">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">list</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="lists_getIndex">' +
|
||||
'<value name="VALUE">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">list</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="lists_setIndex">' +
|
||||
'<value name="LIST">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">list</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="lists_getSublist">' +
|
||||
'<value name="LIST">' +
|
||||
'<block type="variables_get">' +
|
||||
'<field name="VAR">list</field>' +
|
||||
'</block>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="lists_split">' +
|
||||
'<value name="DELIM">' +
|
||||
'<shadow type="text">' +
|
||||
'<field name="TEXT">,</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="lists_sort"></block>' +
|
||||
'</xml>');
|
||||
FactoryController.prototype.standardCategories['lists'].color = '#745CA6';
|
||||
|
||||
FactoryController.prototype.standardCategories['colour'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Colour');
|
||||
FactoryController.prototype.standardCategories['colour'].xml =
|
||||
Blockly.Xml.textToDom(
|
||||
'<xml>' +
|
||||
'<block type="colour_picker"></block>' +
|
||||
'<block type="colour_random"></block>' +
|
||||
'<block type="colour_rgb">' +
|
||||
'<value name="RED">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">100</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="GREEN">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">50</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="BLUE">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">0</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'<block type="colour_blend">' +
|
||||
'<value name="COLOUR1">' +
|
||||
'<shadow type="colour_picker">' +
|
||||
'<field name="COLOUR">#ff0000</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="COLOUR2">' +
|
||||
'<shadow type="colour_picker">' +
|
||||
'<field name="COLOUR">#3333ff</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'<value name="RATIO">' +
|
||||
'<shadow type="math_number">' +
|
||||
'<field name="NUM">0.5</field>' +
|
||||
'</shadow>' +
|
||||
'</value>' +
|
||||
'</block>' +
|
||||
'</xml>');
|
||||
FactoryController.prototype.standardCategories['colour'].color = '#A6745C';
|
||||
|
||||
FactoryController.prototype.standardCategories['functions'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Functions');
|
||||
FactoryController.prototype.standardCategories['functions'].color = '#9A5CA6'
|
||||
FactoryController.prototype.standardCategories['functions'].custom =
|
||||
'PROCEDURE';
|
||||
|
||||
FactoryController.prototype.standardCategories['variables'] =
|
||||
new ListElement(ListElement.TYPE_CATEGORY, 'Variables');
|
||||
FactoryController.prototype.standardCategories['variables'].color = '#A65C81';
|
||||
FactoryController.prototype.standardCategories['variables'].custom = 'VARIABLE';
|
||||
238
demos/blocklyfactory/workspacefactory/style.css
Normal file
238
demos/blocklyfactory/workspacefactory/style.css
Normal file
@@ -0,0 +1,238 @@
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
section {
|
||||
float: left;
|
||||
}
|
||||
|
||||
aside {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#categoryTable>table {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td.tabon {
|
||||
border-bottom-color: #ddd !important;
|
||||
background-color: #ddd;
|
||||
padding: 5px 19px;
|
||||
}
|
||||
|
||||
td.taboff {
|
||||
cursor: pointer;
|
||||
padding: 5px 19px;
|
||||
}
|
||||
|
||||
td.taboff:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #eee;
|
||||
color: #000;
|
||||
font-size: large;
|
||||
margin: 0 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
box-shadow: 2px 2px 5px #888;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
button>* {
|
||||
opacity: .6;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled)>* {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #eee;
|
||||
color: #000;
|
||||
font-size: large;
|
||||
margin: 0 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
label:hover:not(:disabled) {
|
||||
box-shadow: 2px 2px 5px #888;
|
||||
}
|
||||
|
||||
label:disabled {
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
label>* {
|
||||
opacity: .6;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
label:hover:not(:disabled)>* {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.inputfile {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#toolbox_section {
|
||||
height: 480px;
|
||||
width: 80%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#toolbox_blocks {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#preview_blocks {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#createDiv {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
#previewDiv {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#category_section {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#disable_div {
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: .5;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: -1; /* Start behind workspace */
|
||||
}
|
||||
|
||||
/* Rules for Closure popup color picker */
|
||||
.goog-palette {
|
||||
outline: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.goog-palette-cell {
|
||||
height: 13px;
|
||||
width: 15px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-right: 1px solid #000000;
|
||||
font-size: 1px;
|
||||
}
|
||||
|
||||
.goog-palette-colorswatch {
|
||||
border: 1px solid #000000;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.goog-palette-cell-hover .goog-palette-colorswatch {
|
||||
border: 1px solid #FFF;
|
||||
}
|
||||
|
||||
.goog-palette-cell-selected .goog-palette-colorswatch {
|
||||
border: 1px solid #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.goog-palette-table {
|
||||
border: 1px solid #000;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.goog-popupcolorpicker {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* The container <div> - needed to position the dropdown content */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Dropdown Content (Hidden by Default) */
|
||||
.dropdown-content {
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,.2);
|
||||
display: none;
|
||||
min-width: 170px;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Links inside the dropdown */
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
display: block;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Change color of dropdown links on hover */
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f1f1f1
|
||||
}
|
||||
|
||||
/* Show the dropdown menu */
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.shadowBlock>.blocklyPath {
|
||||
fill-opacity: .5;
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.shadowBlock>.blocklyPathLight,
|
||||
.shadowBlock>.blocklyPathDark {
|
||||
display: none;
|
||||
}
|
||||
704
demos/blocklyfactory/workspacefactory/wfactory_controller.js
Normal file
704
demos/blocklyfactory/workspacefactory/wfactory_controller.js
Normal file
@@ -0,0 +1,704 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Contains the controller code for workspace factory. Depends
|
||||
* on the model and view objects (created as internal variables) and interacts
|
||||
* with previewWorkspace and toolboxWorkspace (internal references stored to
|
||||
* both). Also depends on standard_categories.js for standard Blockly
|
||||
* categories. Provides the functionality for the actions the user can initiate:
|
||||
* - adding and removing categories
|
||||
* - switching between categories
|
||||
* - printing and downloading configuration xml
|
||||
* - updating the preview workspace
|
||||
* - changing a category name
|
||||
* - moving the position of a category.
|
||||
*
|
||||
* @author Emma Dauterman (evd2014)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for a FactoryController
|
||||
* @constructor
|
||||
* @param {!Blockly.workspace} toolboxWorkspace workspace where blocks are
|
||||
* dragged into corresponding categories
|
||||
* @param {!Blockly.workspace} previewWorkspace workspace that shows preview
|
||||
* of what workspace would look like using generated XML
|
||||
*/
|
||||
FactoryController = function(toolboxWorkspace, previewWorkspace) {
|
||||
// Workspace for user to drag blocks in for a certain category.
|
||||
this.toolboxWorkspace = toolboxWorkspace;
|
||||
// Workspace for user to preview their changes.
|
||||
this.previewWorkspace = previewWorkspace;
|
||||
// Model to keep track of categories and blocks.
|
||||
this.model = new FactoryModel();
|
||||
// Updates the category tabs.
|
||||
this.view = new FactoryView();
|
||||
// Generates XML for categories.
|
||||
this.generator = new FactoryGenerator(this.model);
|
||||
};
|
||||
|
||||
/**
|
||||
* Currently prompts the user for a name, checking that it's valid (not used
|
||||
* before), and then creates a tab and switches to it.
|
||||
*/
|
||||
FactoryController.prototype.addCategory = function() {
|
||||
// Check if it's the first category added.
|
||||
var firstCategory = !this.model.hasToolbox();
|
||||
// Give the option to save blocks if their workspace is not empty and they
|
||||
// are creating their first category.
|
||||
if (firstCategory && this.toolboxWorkspace.getAllBlocks().length > 0) {
|
||||
var confirmCreate = confirm('Do you want to save your work in another '
|
||||
+ 'category? If you don\'t, the blocks in your workspace will be ' +
|
||||
'deleted.');
|
||||
// Create a new category for current blocks.
|
||||
if (confirmCreate) {
|
||||
var name = prompt('Enter the name of the category for your ' +
|
||||
'current blocks: ');
|
||||
if (!name) { // Exit if cancelled.
|
||||
return;
|
||||
}
|
||||
this.createCategory(name, true);
|
||||
this.model.setSelectedById(this.model.getCategoryIdByName(name));
|
||||
}
|
||||
}
|
||||
// After possibly creating a category, check again if it's the first category.
|
||||
firstCategory = !this.model.hasToolbox();
|
||||
// Get name from user.
|
||||
name = this.promptForNewCategoryName('Enter the name of your new category: ');
|
||||
if (!name) { //Exit if cancelled.
|
||||
return;
|
||||
}
|
||||
// Create category.
|
||||
this.createCategory(name, firstCategory);
|
||||
// Switch to category.
|
||||
this.switchElement(this.model.getCategoryIdByName(name));
|
||||
// Update preview.
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper method for addCategory. Adds a category to the view given a name, ID,
|
||||
* and a boolean for if it's the first category created. Assumes the category
|
||||
* has already been created in the model. Does not switch to category.
|
||||
*
|
||||
* @param {!string} name Name of category being added.
|
||||
* @param {!string} id The ID of the category being added.
|
||||
* @param {boolean} firstCategory True if it's the first category created,
|
||||
* false otherwise.
|
||||
*/
|
||||
FactoryController.prototype.createCategory = function(name, firstCategory) {
|
||||
// Create empty category
|
||||
var category = new ListElement(ListElement.TYPE_CATEGORY, name);
|
||||
this.model.addElementToList(category);
|
||||
// Create new category.
|
||||
var tab = this.view.addCategoryRow(name, category.id, firstCategory);
|
||||
this.addClickToSwitch(tab, category.id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a tab and a ID to be associated to that tab, adds a listener to
|
||||
* that tab so that when the user clicks on the tab, it switches to the
|
||||
* element associated with that ID.
|
||||
*
|
||||
* @param {!Element} tab The DOM element to add the listener to.
|
||||
* @param {!string} id The ID of the element to switch to when tab is clicked.
|
||||
*/
|
||||
FactoryController.prototype.addClickToSwitch = function(tab, id) {
|
||||
var self = this;
|
||||
var clickFunction = function(id) { // Keep this in scope for switchElement
|
||||
return function() {
|
||||
self.switchElement(id);
|
||||
};
|
||||
};
|
||||
this.view.bindClick(tab, clickFunction(id));
|
||||
};
|
||||
|
||||
/**
|
||||
* Attached to "-" button. Checks if the user wants to delete
|
||||
* the current element. Removes the element and switches to another element.
|
||||
* When the last element is removed, it switches to a single flyout mode.
|
||||
*
|
||||
*/
|
||||
FactoryController.prototype.removeElement = function() {
|
||||
// Check that there is a currently selected category to remove.
|
||||
if (!this.model.getSelected()) {
|
||||
return;
|
||||
}
|
||||
// Check if user wants to remove current category.
|
||||
var check = confirm('Are you sure you want to delete the currently selected '
|
||||
+ this.model.getSelected().type + '?');
|
||||
if (!check) { // If cancelled, exit.
|
||||
return;
|
||||
}
|
||||
var selectedId = this.model.getSelectedId();
|
||||
var selectedIndex = this.model.getIndexByElementId(selectedId);
|
||||
// Delete element visually.
|
||||
this.view.deleteElementRow(selectedId, selectedIndex);
|
||||
// Delete element in model.
|
||||
this.model.deleteElementFromList(selectedIndex);
|
||||
// Find next logical element to switch to.
|
||||
var next = this.model.getElementByIndex(selectedIndex);
|
||||
if (!next && this.model.hasToolbox()) {
|
||||
next = this.model.getElementByIndex(selectedIndex - 1);
|
||||
}
|
||||
var nextId = next ? next.id : null;
|
||||
// Open next element.
|
||||
this.clearAndLoadElement(nextId);
|
||||
if (!nextId) {
|
||||
alert('You currently have no categories or separators. All your blocks' +
|
||||
' will be displayed in a single flyout.');
|
||||
}
|
||||
// Update preview.
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a valid name for a new category from the user.
|
||||
*
|
||||
* @param {!string} promptString Prompt for the user to enter a name.
|
||||
* @return {string} Valid name for a new category, or null if cancelled.
|
||||
*/
|
||||
FactoryController.prototype.promptForNewCategoryName = function(promptString) {
|
||||
do {
|
||||
var name = prompt(promptString);
|
||||
if (!name) { // If cancelled.
|
||||
return null;
|
||||
}
|
||||
} while (this.model.hasCategoryByName(name));
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a new tab for the element given by ID. Stores XML and blocks
|
||||
* to reload later, updates selected accordingly, and clears the workspace
|
||||
* and clears undo, then loads the new element.
|
||||
*
|
||||
* @param {!string} id ID of tab to be opened, must be valid element ID.
|
||||
*/
|
||||
FactoryController.prototype.switchElement = function(id) {
|
||||
// Disables events while switching so that Blockly delete and create events
|
||||
// don't update the preview repeatedly.
|
||||
Blockly.Events.disable();
|
||||
// Caches information to reload or generate xml if switching to/from element.
|
||||
// Only saves if a category is selected.
|
||||
if (this.model.getSelectedId() != null && id != null) {
|
||||
this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
|
||||
}
|
||||
// Load element.
|
||||
this.clearAndLoadElement(id);
|
||||
// Enable Blockly events again.
|
||||
Blockly.Events.enable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Switches to a new tab for the element by ID. Helper for switchElement.
|
||||
* Updates selected, clears the workspace and clears undo, loads a new element.
|
||||
*
|
||||
* @param {!string} id ID of category to load
|
||||
*/
|
||||
FactoryController.prototype.clearAndLoadElement = function(id) {
|
||||
// Unselect current tab if switching to and from an element.
|
||||
if (this.model.getSelectedId() != null && id != null) {
|
||||
this.view.setCategoryTabSelection(this.model.getSelectedId(), false);
|
||||
}
|
||||
|
||||
// If switching from a separator, enable workspace in view.
|
||||
if (this.model.getSelectedId() != null && this.model.getSelected().type ==
|
||||
ListElement.TYPE_SEPARATOR) {
|
||||
this.view.disableWorkspace(false);
|
||||
}
|
||||
|
||||
// Set next category.
|
||||
this.model.setSelectedById(id);
|
||||
|
||||
// Clear workspace.
|
||||
this.toolboxWorkspace.clear();
|
||||
this.toolboxWorkspace.clearUndo();
|
||||
|
||||
// Loads next category if switching to an element.
|
||||
if (id != null) {
|
||||
this.view.setCategoryTabSelection(id, true);
|
||||
Blockly.Xml.domToWorkspace(this.model.getSelectedXml(),
|
||||
this.toolboxWorkspace);
|
||||
// Disable workspace if switching to a separator.
|
||||
if (this.model.getSelected().type == ListElement.TYPE_SEPARATOR) {
|
||||
this.view.disableWorkspace(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all shadow blocks laoded and order blocks as if shown in a flyout.
|
||||
this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace
|
||||
(toolboxWorkspace.getAllBlocks()));
|
||||
this.toolboxWorkspace.cleanUp_();
|
||||
|
||||
// Update category editing buttons.
|
||||
this.view.updateState(this.model.getIndexByElementId
|
||||
(this.model.getSelectedId()), this.model.getSelected());
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to "Export Config" button. Gets a file name from the user and downloads
|
||||
* the corresponding configuration xml to that file.
|
||||
*/
|
||||
FactoryController.prototype.exportConfig = function() {
|
||||
// Generate XML.
|
||||
var configXml = Blockly.Xml.domToPrettyText
|
||||
(this.generator.generateConfigXml(this.toolboxWorkspace));
|
||||
// Get file name.
|
||||
var fileName = prompt("File Name: ");
|
||||
if (!fileName) { // If cancelled
|
||||
return;
|
||||
}
|
||||
// Download file.
|
||||
var data = new Blob([configXml], {type: 'text/xml'});
|
||||
this.view.createAndDownloadFile(fileName, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to "Print Config" button. Mainly used for debugging purposes. Prints
|
||||
* the configuration XML to the console.
|
||||
*/
|
||||
FactoryController.prototype.printConfig = function() {
|
||||
window.console.log(Blockly.Xml.domToPrettyText
|
||||
(this.generator.generateConfigXml(this.toolboxWorkspace)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the preview workspace based on the toolbox workspace. If switching
|
||||
* from no categories to categories or categories to no categories, reinjects
|
||||
* Blockly with reinjectPreview, otherwise just updates without reinjecting.
|
||||
* Called whenever a list element is created, removed, or modified and when
|
||||
* Blockly move and delete events are fired. Do not call on create events
|
||||
* or disabling will cause the user to "drop" their current blocks.
|
||||
*/
|
||||
FactoryController.prototype.updatePreview = function() {
|
||||
// Disable events to stop updatePreview from recursively calling itself
|
||||
// through event handlers.
|
||||
Blockly.Events.disable();
|
||||
var tree = Blockly.Options.parseToolboxTree
|
||||
(this.generator.generateConfigXml(this.toolboxWorkspace));
|
||||
// No categories, creates a simple flyout.
|
||||
if (tree.getElementsByTagName('category').length == 0) {
|
||||
if (this.previewWorkspace.toolbox_) {
|
||||
this.reinjectPreview(tree); // Switch to simple flyout, more expensive.
|
||||
} else {
|
||||
this.previewWorkspace.flyout_.show(tree.childNodes);
|
||||
}
|
||||
// Uses categories, creates a toolbox.
|
||||
} else {
|
||||
if (!previewWorkspace.toolbox_) {
|
||||
this.reinjectPreview(tree); // Create a toolbox, more expensive.
|
||||
} else {
|
||||
this.previewWorkspace.toolbox_.populate_(tree);
|
||||
}
|
||||
}
|
||||
// Reenable events.
|
||||
Blockly.Events.enable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to completely reinject the preview workspace. This should be used only
|
||||
* when switching from simple flyout to categories, or categories to simple
|
||||
* flyout. More expensive than simply updating the flyout or toolbox.
|
||||
*
|
||||
* @param {!Element} tree of xml elements
|
||||
* @package
|
||||
*/
|
||||
FactoryController.prototype.reinjectPreview = function(tree) {
|
||||
this.previewWorkspace.dispose();
|
||||
previewToolbox = Blockly.Xml.domToPrettyText(tree);
|
||||
this.previewWorkspace = Blockly.inject('preview_blocks',
|
||||
{grid:
|
||||
{spacing: 25,
|
||||
length: 3,
|
||||
colour: '#ccc',
|
||||
snap: true},
|
||||
media: '../../../media/',
|
||||
toolbox: previewToolbox,
|
||||
zoom:
|
||||
{controls: true,
|
||||
wheel: true}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to "change name" button. Changes the name of the selected category.
|
||||
* Continues prompting the user until they input a category name that is not
|
||||
* currently in use, exits if user presses cancel.
|
||||
*/
|
||||
FactoryController.prototype.changeCategoryName = function() {
|
||||
// Return if no category selected or element a separator.
|
||||
if (!this.model.getSelected() ||
|
||||
this.model.getSelected().type == ListElement.TYPE_SEPARATOR) {
|
||||
return;
|
||||
}
|
||||
// Get new name from user.
|
||||
var newName = this.promptForNewCategoryName('What do you want to change this'
|
||||
+ ' category\'s name to?');
|
||||
if (!newName) { // If cancelled.
|
||||
return;
|
||||
}
|
||||
// Change category name.
|
||||
this.model.getSelected().changeName(newName);
|
||||
this.view.updateCategoryName(newName, this.model.getSelectedId());
|
||||
// Update preview.
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to arrow up and arrow down buttons. Swaps with the element above or
|
||||
* below the currently selected element (offset categories away from the
|
||||
* current element). Updates state to enable the correct element editing
|
||||
* buttons.
|
||||
*
|
||||
* @param {int} offset The index offset from the currently selected element
|
||||
* to swap with. Positive if the element to be swapped with is below, negative
|
||||
* if the element to be swapped with is above.
|
||||
*/
|
||||
FactoryController.prototype.moveElement = function(offset) {
|
||||
var curr = this.model.getSelected();
|
||||
if (!curr) { // Return if no selected element.
|
||||
return;
|
||||
}
|
||||
var currIndex = this.model.getIndexByElementId(curr.id);
|
||||
var swapIndex = this.model.getIndexByElementId(curr.id) + offset;
|
||||
var swap = this.model.getElementByIndex(swapIndex);
|
||||
if (!swap) { // Return if cannot swap in that direction.
|
||||
return;
|
||||
}
|
||||
// Move currently selected element to index of other element.
|
||||
// Indexes must be valid because confirmed that curr and swap exist.
|
||||
this.moveElementToIndex(curr, swapIndex, currIndex);
|
||||
// Update element editing buttons.
|
||||
this.view.updateState(swapIndex, this.model.getSelected());
|
||||
// Update preview.
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves a element to a specified index and updates the model and view
|
||||
* accordingly. Helper functions throw an error if indexes are out of bounds.
|
||||
*
|
||||
* @param {!Element} element The element to move.
|
||||
* @param {int} newIndex The index to insert the element at.
|
||||
* @param {int} oldIndex The index the element is currently at.
|
||||
*/
|
||||
FactoryController.prototype.moveElementToIndex = function(element, newIndex,
|
||||
oldIndex) {
|
||||
this.model.moveElementToIndex(element, newIndex, oldIndex);
|
||||
this.view.moveTabToIndex(element.id, newIndex, oldIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the color of the selected category. Return if selected element is
|
||||
* a separator.
|
||||
*
|
||||
* @param {!string} color The color to change the selected category. Must be
|
||||
* a valid CSS string.
|
||||
*/
|
||||
FactoryController.prototype.changeSelectedCategoryColor = function(color) {
|
||||
// Return if no category selected or element a separator.
|
||||
if (!this.model.getSelected() ||
|
||||
this.model.getSelected().type == ListElement.TYPE_SEPARATOR) {
|
||||
return;
|
||||
}
|
||||
// Change color of selected category.
|
||||
this.model.getSelected().changeColor(color);
|
||||
this.view.setBorderColor(this.model.getSelectedId(), color);
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tied to the "Standard Category" dropdown option, this function prompts
|
||||
* the user for a name of a standard Blockly category (case insensitive) and
|
||||
* loads it as a new category and switches to it. Leverages standardCategories
|
||||
* map in standard_categories.js.
|
||||
*/
|
||||
FactoryController.prototype.loadCategory = function() {
|
||||
// Prompt user for the name of the standard category to load.
|
||||
do {
|
||||
var name = prompt('Enter the name of the category you would like to import '
|
||||
+ '(Logic, Loops, Math, Text, Lists, Colour, Variables, or Functions)');
|
||||
if (!name) {
|
||||
return; // Exit if cancelled.
|
||||
}
|
||||
} while (!this.isStandardCategoryName(name));
|
||||
|
||||
// Check if the user can create that standard category.
|
||||
if (this.model.hasVariables() && name.toLowerCase() == 'variables') {
|
||||
alert('A Variables category already exists. You cannot create multiple' +
|
||||
' variables categories.');
|
||||
return;
|
||||
}
|
||||
if (this.model.hasProcedures() && name.toLowerCase() == 'functions') {
|
||||
alert('A Functions category already exists. You cannot create multiple' +
|
||||
' functions categories.');
|
||||
return;
|
||||
}
|
||||
// Check if the user can create a category with that name.
|
||||
var standardCategory = this.standardCategories[name.toLowerCase()]
|
||||
if (this.model.hasCategoryByName(standardCategory.name)) {
|
||||
alert('You already have a category with the name ' + standardCategory.name
|
||||
+ '. Rename your category and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the standard category in the model.
|
||||
var copy = standardCategory.copy();
|
||||
|
||||
// Add the copy in the view.
|
||||
var tab = this.view.addCategoryRow(copy.name, copy.id,
|
||||
!this.model.hasToolbox());
|
||||
|
||||
// Add it to the model.
|
||||
this.model.addElementToList(copy);
|
||||
|
||||
// Update the view.
|
||||
this.addClickToSwitch(tab, copy.id);
|
||||
// Color the category tab in the view.
|
||||
if (copy.color) {
|
||||
this.view.setBorderColor(copy.id, copy.color);
|
||||
}
|
||||
// Switch to loaded category.
|
||||
this.switchElement(copy.id);
|
||||
// Convert actual shadow blocks to user-generated shadow blocks.
|
||||
this.convertShadowBlocks();
|
||||
// Update preview.
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the name of a category, determines if it's the name of a standard
|
||||
* category (case insensitive).
|
||||
*
|
||||
* @param {string} name The name of the category that should be checked if it's
|
||||
* in standardCategories
|
||||
* @return {boolean} True if name is a standard category name, false otherwise.
|
||||
*/
|
||||
FactoryController.prototype.isStandardCategoryName = function(name) {
|
||||
for (var category in this.standardCategories) {
|
||||
if (name.toLowerCase() == category) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Connected to the "add separator" dropdown option. If categories already
|
||||
* exist, adds a separator to the model and view. Does not switch to select
|
||||
* the separator, and updates the preview.
|
||||
*/
|
||||
FactoryController.prototype.addSeparator = function() {
|
||||
// Don't allow the user to add a separator if a category has not been created.
|
||||
if (!this.model.hasToolbox()) {
|
||||
alert('Add a category before adding a separator.');
|
||||
return;
|
||||
}
|
||||
// Create the separator in the model.
|
||||
var separator = new ListElement(ListElement.TYPE_SEPARATOR);
|
||||
this.model.addElementToList(separator);
|
||||
// Create the separator in the view.
|
||||
var tab = this.view.addSeparatorTab(separator.id);
|
||||
this.addClickToSwitch(tab, separator.id);
|
||||
// Switch to the separator and update the preview.
|
||||
this.switchElement(separator.id);
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Connected to the import button. Given the file path inputted by the user
|
||||
* from file input, this function loads that toolbox XML to the workspace,
|
||||
* creating category and separator tabs as necessary. This allows the user
|
||||
* to be able to edit toolboxes given their XML form. Catches errors from
|
||||
* file reading and prints an error message alerting the user.
|
||||
*
|
||||
* @param {string} file The path for the file to be imported into the workspace.
|
||||
* Should contain valid toolbox XML.
|
||||
*/
|
||||
FactoryController.prototype.importFile = function(file) {
|
||||
// Exit if cancelled.
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new FileReader();
|
||||
// To be executed when the reader has read the file.
|
||||
reader.onload = function() {
|
||||
// Try to parse XML from file and load it into toolbox editing area.
|
||||
// Print error message if fail.
|
||||
try {
|
||||
var tree = Blockly.Xml.textToDom(reader.result);
|
||||
controller.importFromTree_(tree);
|
||||
} catch(e) {
|
||||
alert('Cannot load XML from file.');
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the file.
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a XML DOM tree, loads it into the toolbox editing area so that the
|
||||
* user can continue editing their work. Assumes that tree is in valid toolbox
|
||||
* XML format.
|
||||
* @private
|
||||
*
|
||||
* @param {!Element} tree XML tree to be loaded to toolbox editing area.
|
||||
*/
|
||||
FactoryController.prototype.importFromTree_ = function(tree) {
|
||||
// Clear current editing area.
|
||||
this.model.clearToolboxList();
|
||||
this.view.clearToolboxTabs();
|
||||
|
||||
if (tree.getElementsByTagName('category').length == 0) {
|
||||
// No categories present.
|
||||
// Load all the blocks into a single category evenly spaced.
|
||||
Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace);
|
||||
this.toolboxWorkspace.cleanUp_();
|
||||
|
||||
// Convert actual shadow blocks to user-generated shadow blocks.
|
||||
this.convertShadowBlocks();
|
||||
|
||||
// Add message to denote empty category.
|
||||
this.view.addEmptyCategoryMessage();
|
||||
} else {
|
||||
// Categories/separators present.
|
||||
for (var i = 0, item; item = tree.children[i]; i++) {
|
||||
if (item.tagName == 'category') {
|
||||
// If the element is a category, create a new category and switch to it.
|
||||
this.createCategory(item.getAttribute('name'), false);
|
||||
var category = this.model.getElementByIndex(i);
|
||||
this.switchElement(category.id);
|
||||
|
||||
// Load all blocks in that category to the workspace to be evenly
|
||||
// spaced and saved to that category.
|
||||
for (var j = 0, blockXml; blockXml = item.children[j]; j++) {
|
||||
Blockly.Xml.domToBlock(blockXml, this.toolboxWorkspace);
|
||||
}
|
||||
|
||||
// Evenly space the blocks.
|
||||
// TODO(evd2014): Change to cleanUp once cleanUp_ is made public in
|
||||
// master.
|
||||
this.toolboxWorkspace.cleanUp_();
|
||||
|
||||
// Convert actual shadow blocks to user-generated shadow blocks.
|
||||
this.convertShadowBlocks();
|
||||
|
||||
// Set category color.
|
||||
if (item.getAttribute('colour')) {
|
||||
category.changeColor(item.getAttribute('colour'));
|
||||
this.view.setBorderColor(category.id, category.color);
|
||||
}
|
||||
// Set any custom tags.
|
||||
if (item.getAttribute('custom')) {
|
||||
this.model.addCustomTag(category, item.getAttribute('custom'));
|
||||
}
|
||||
} else {
|
||||
// If the element is a separator, add the separator and switch to it.
|
||||
this.addSeparator();
|
||||
this.switchElement(this.model.getElementByIndex(i).id);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.view.updateState(this.model.getIndexByElementId
|
||||
(this.model.getSelectedId()), this.model.getSelected());
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the toolbox editing area completely, deleting all categories and all
|
||||
* blocks in the model and view.
|
||||
*/
|
||||
FactoryController.prototype.clear = function() {
|
||||
this.model.clearToolboxList();
|
||||
this.view.clearToolboxTabs();
|
||||
this.view.addEmptyCategoryMessage();
|
||||
this.view.updateState(-1, null);
|
||||
this.toolboxWorkspace.clear();
|
||||
this.toolboxWorkspace.clearUndo();
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/*
|
||||
* Makes the currently selected block a user-generated shadow block. These
|
||||
* blocks are not made into real shadow blocks, but recorded in the model
|
||||
* and visually marked as shadow blocks, allowing the user to move and edit
|
||||
* them (which would be impossible with actual shadow blocks). Updates the
|
||||
* preview when done.
|
||||
*
|
||||
*/
|
||||
FactoryController.prototype.addShadow = function() {
|
||||
// No block selected to make a shadow block.
|
||||
if (!Blockly.selected) {
|
||||
return;
|
||||
}
|
||||
this.view.markShadowBlock(Blockly.selected);
|
||||
this.model.addShadowBlock(Blockly.selected.id);
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* If the currently selected block is a user-generated shadow block, this
|
||||
* function makes it a normal block again, removing it from the list of
|
||||
* shadow blocks and loading the workspace again. Updates the preview again.
|
||||
*
|
||||
*/
|
||||
FactoryController.prototype.removeShadow = function() {
|
||||
// No block selected to modify.
|
||||
if (!Blockly.selected) {
|
||||
return;
|
||||
}
|
||||
this.model.removeShadowBlock(Blockly.selected.id);
|
||||
this.view.unmarkShadowBlock(Blockly.selected);
|
||||
this.updatePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a unique block ID, uses the model to determine if a block is a
|
||||
* user-generated shadow block.
|
||||
*
|
||||
* @param {!string} blockId The unique ID of the block to examine.
|
||||
* @return {boolean} True if the block is a user-generated shadow block, false
|
||||
* otherwise.
|
||||
*/
|
||||
FactoryController.prototype.isUserGenShadowBlock = function(blockId) {
|
||||
return this.model.isShadowBlock(blockId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when importing XML containing real shadow blocks. This function turns
|
||||
* all real shadow blocks loaded in the workspace into user-generated shadow
|
||||
* blocks, meaning they are marked as shadow blocks by the model and appear as
|
||||
* shadow blocks in the view but are still editable and movable.
|
||||
*/
|
||||
FactoryController.prototype.convertShadowBlocks = function() {
|
||||
var blocks = this.toolboxWorkspace.getAllBlocks();
|
||||
for (var i = 0, block; block = blocks[i]; i++) {
|
||||
if (block.isShadow()) {
|
||||
block.setShadow(false);
|
||||
this.model.addShadowBlock(block.id);
|
||||
this.view.markShadowBlock(block);
|
||||
}
|
||||
}
|
||||
};
|
||||
159
demos/blocklyfactory/workspacefactory/wfactory_generator.js
Normal file
159
demos/blocklyfactory/workspacefactory/wfactory_generator.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Generates the configuration xml used to update the preview
|
||||
* workspace or print to the console or download to a file. Leverages
|
||||
* Blockly.Xml and depends on information in the model (holds a reference).
|
||||
* Depends on a hidden workspace created in the generator to load saved XML in
|
||||
* order to generate toolbox XML.
|
||||
*
|
||||
* @author Emma Dauterman (evd2014)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for a FactoryGenerator
|
||||
* @constructor
|
||||
*/
|
||||
FactoryGenerator = function(model) {
|
||||
// Model to share information about categories and shadow blocks.
|
||||
this.model = model;
|
||||
// Create hidden workspace to load saved XML to generate toolbox XML.
|
||||
var hiddenBlocks = document.createElement('div');
|
||||
// Generate a globally unique ID for the hidden div element to avoid
|
||||
// collisions.
|
||||
var hiddenBlocksId = Blockly.genUid();
|
||||
hiddenBlocks.id = hiddenBlocksId;
|
||||
hiddenBlocks.style.display = 'none';
|
||||
document.body.appendChild(hiddenBlocks);
|
||||
this.hiddenWorkspace = Blockly.inject(hiddenBlocksId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the xml for the toolbox or flyout with information from
|
||||
* toolboxWorkspace and the model. Uses the hiddenWorkspace to generate XML.
|
||||
*
|
||||
* @param {!Blockly.workspace} toolboxWorkspace Toolbox editing workspace where
|
||||
* blocks are added by user to be part of the toolbox.
|
||||
* @return {!Element} XML element representing toolbox or flyout corresponding
|
||||
* to toolbox workspace.
|
||||
*/
|
||||
FactoryGenerator.prototype.generateConfigXml = function(toolboxWorkspace) {
|
||||
// Create DOM for XML.
|
||||
var xmlDom = goog.dom.createDom('xml',
|
||||
{
|
||||
'id' : 'toolbox',
|
||||
'style' : 'display:none'
|
||||
});
|
||||
if (!this.model.hasToolbox()) {
|
||||
// Toolbox has no categories. Use XML directly from workspace.
|
||||
this.loadToHiddenWorkspaceAndSave_
|
||||
(Blockly.Xml.workspaceToDom(toolboxWorkspace), xmlDom);
|
||||
} else {
|
||||
// Toolbox has categories.
|
||||
// Assert that selected != null
|
||||
if (!this.model.getSelected()) {
|
||||
throw new Error('Selected is null when the toolbox is empty.');
|
||||
}
|
||||
|
||||
// Capture any changes made by user before generating XML.
|
||||
this.model.getSelected().saveFromWorkspace(toolboxWorkspace);
|
||||
var xml = this.model.getSelectedXml();
|
||||
var toolboxList = this.model.getToolboxList();
|
||||
|
||||
// Iterate through each category to generate XML for each using the
|
||||
// hidden workspace. Load each category to the hidden workspace to make sure
|
||||
// that all the blocks that are not top blocks are also captured as block
|
||||
// groups in the flyout.
|
||||
for (var i = 0; i < toolboxList.length; i++) {
|
||||
var element = toolboxList[i];
|
||||
if (element.type == ListElement.TYPE_SEPARATOR) {
|
||||
// If the next element is a separator.
|
||||
var nextElement = goog.dom.createDom('sep');
|
||||
} else {
|
||||
// If the next element is a category.
|
||||
var nextElement = goog.dom.createDom('category');
|
||||
nextElement.setAttribute('name', element.name);
|
||||
// Add a colour attribute if one exists.
|
||||
if (element.color != null) {
|
||||
nextElement.setAttribute('colour', element.color);
|
||||
}
|
||||
// Add a custom attribute if one exists.
|
||||
if (element.custom != null) {
|
||||
nextElement.setAttribute('custom', element.custom);
|
||||
}
|
||||
// Load that category to hidden workspace, setting user-generated shadow
|
||||
// blocks as real shadow blocks.
|
||||
this.loadToHiddenWorkspaceAndSave_(element.xml, nextElement);
|
||||
}
|
||||
xmlDom.appendChild(nextElement);
|
||||
}
|
||||
}
|
||||
return xmlDom;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the given XML to the hidden workspace, set any user-generated shadow
|
||||
* blocks to be actual shadow blocks, then append the XML from the workspace
|
||||
* to the DOM element passed in.
|
||||
* @private
|
||||
*
|
||||
* @param {!Element} xml The XML to be loaded to the hidden workspace.
|
||||
* @param {!Element} dom The DOM element to append the generated XML to.
|
||||
*/
|
||||
FactoryGenerator.prototype.loadToHiddenWorkspaceAndSave_ = function(xml, dom) {
|
||||
this.hiddenWorkspace.clear();
|
||||
Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace);
|
||||
this.setShadowBlocksInHiddenWorkspace_();
|
||||
this.appendHiddenWorkspaceToDom_(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes blocks in the hidden workspace in a XML DOM element. Very
|
||||
* similar to workspaceToDom, but doesn't capture IDs. Uses the top-level
|
||||
* blocks loaded in hiddenWorkspace.
|
||||
* @private
|
||||
*
|
||||
* @param {!Element} xmlDom Tree of XML elements to be appended to.
|
||||
*/
|
||||
FactoryGenerator.prototype.appendHiddenWorkspaceToDom_ = function(xmlDom) {
|
||||
var blocks = this.hiddenWorkspace.getTopBlocks();
|
||||
for (var i = 0, block; block = blocks[i]; i++) {
|
||||
var blockChild = Blockly.Xml.blockToDom(block);
|
||||
blockChild.removeAttribute('id');
|
||||
xmlDom.appendChild(blockChild);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the user-generated shadow blocks loaded into hiddenWorkspace to be
|
||||
* actual shadow blocks. This is done so that blockToDom records them as
|
||||
* shadow blocks instead of regular blocks.
|
||||
* @private
|
||||
*
|
||||
*/
|
||||
FactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = function() {
|
||||
var blocks = this.hiddenWorkspace.getAllBlocks();
|
||||
for (var i = 0; i < blocks.length; i++) {
|
||||
if (this.model.isShadowBlock(blocks[i].id)) {
|
||||
blocks[i].setShadow(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
450
demos/blocklyfactory/workspacefactory/wfactory_model.js
Normal file
450
demos/blocklyfactory/workspacefactory/wfactory_model.js
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Stores and updates information about state and categories
|
||||
* in workspace factory. Each list element is either a separator or a category,
|
||||
* and each category stores its name, XML to load that category, color,
|
||||
* custom tags, and a unique ID making it possible to change category names and
|
||||
* move categories easily. Keeps track of the currently selected list
|
||||
* element. Also keeps track of all the user-created shadow blocks and
|
||||
* manipulates them as necessary.
|
||||
*
|
||||
* @author Emma Dauterman (evd2014)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for a FactoryModel
|
||||
* @constructor
|
||||
*/
|
||||
FactoryModel = function() {
|
||||
// Ordered list of ListElement objects.
|
||||
this.toolboxList = [];
|
||||
// Array of block IDs for all user created shadow blocks.
|
||||
this.shadowBlocks = [];
|
||||
// String name of current selected list element, null if no list elements.
|
||||
this.selected = null;
|
||||
// Boolean for if a Variable category has been added.
|
||||
this.hasVariableCategory = false;
|
||||
// Boolean for if a Procedure category has been added.
|
||||
this.hasProcedureCategory = false;
|
||||
};
|
||||
|
||||
// String name of current selected list element, null if no list elements.
|
||||
FactoryModel.prototype.selected = null;
|
||||
|
||||
/**
|
||||
* Given a name, determines if it is the name of a category already present.
|
||||
* Used when getting a valid category name from the user.
|
||||
*
|
||||
* @param {string} name String name to be compared against.
|
||||
* @return {boolean} True if string is a used category name, false otherwise.
|
||||
*/
|
||||
FactoryModel.prototype.hasCategoryByName = function(name) {
|
||||
for (var i = 0; i < this.toolboxList.length; i++) {
|
||||
if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY &&
|
||||
this.toolboxList[i].name == name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a category with the 'VARIABLE' tag exists.
|
||||
*
|
||||
* @return {boolean} True if there exists a category with the Variables tag,
|
||||
* false otherwise.
|
||||
*/
|
||||
FactoryModel.prototype.hasVariables = function() {
|
||||
return this.hasVariableCategory;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a category with the 'PROCEDURE' tag exists.
|
||||
*
|
||||
* @return {boolean} True if there exists a category with the Procedures tag,
|
||||
* false otherwise.
|
||||
*/
|
||||
FactoryModel.prototype.hasProcedures = function() {
|
||||
return this.hasFunctionCategory;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the user has any elements in the toolbox. Uses the length of
|
||||
* toolboxList.
|
||||
*
|
||||
* @return {boolean} True if categories exist, false otherwise.
|
||||
*/
|
||||
FactoryModel.prototype.hasToolbox = function() {
|
||||
return this.toolboxList.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a ListElement, adds it to the toolbox list.
|
||||
*
|
||||
* @param {!ListElement} element The element to be added to the list.
|
||||
*/
|
||||
FactoryModel.prototype.addElementToList = function(element) {
|
||||
// Update state if the copied category has a custom tag.
|
||||
this.hasVariableCategory = element.custom == 'VARIABLE' ? true :
|
||||
this.hasVariableCategory;
|
||||
this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true :
|
||||
this.hasProcedureCategory;
|
||||
// Add element to toolboxList.
|
||||
this.toolboxList.push(element);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an index, deletes a list element and all associated data.
|
||||
*
|
||||
* @param {int} index The index of the list element to delete.
|
||||
*/
|
||||
FactoryModel.prototype.deleteElementFromList = function(index) {
|
||||
// Check if index is out of bounds.
|
||||
if (index < 0 || index >= this.toolboxList.length) {
|
||||
return; // No entry to delete.
|
||||
}
|
||||
// Check if need to update flags.
|
||||
this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ?
|
||||
false : this.hasVariableCategory;
|
||||
this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ?
|
||||
false : this.hasProcedureCategory;
|
||||
// Remove element.
|
||||
this.toolboxList.splice(index, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves a list element to a certain position in toolboxList by removing it
|
||||
* and then inserting it at the correct index. Checks that indices are in
|
||||
* bounds (throws error if not), but assumes that oldIndex is the correct index
|
||||
* for list element.
|
||||
*
|
||||
* @param {!ListElement} element The element to move in toolboxList.
|
||||
* @param {int} newIndex The index to insert the element at.
|
||||
* @param {int} oldIndex The index the element is currently at.
|
||||
*/
|
||||
FactoryModel.prototype.moveElementToIndex = function(element, newIndex,
|
||||
oldIndex) {
|
||||
// Check that indexes are in bounds.
|
||||
if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 ||
|
||||
oldIndex >= this.toolboxList.length) {
|
||||
throw new Error('Index out of bounds when moving element in the model.');
|
||||
}
|
||||
this.deleteElementFromList(oldIndex);
|
||||
this.toolboxList.splice(newIndex, 0, element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the currently selected element. Returns null if there are
|
||||
* no categories (if selected == null).
|
||||
*
|
||||
* @return {string} The ID of the element currently selected.
|
||||
*/
|
||||
FactoryModel.prototype.getSelectedId = function() {
|
||||
return this.selected ? this.selected.id : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the name of the currently selected category. Returns null if there
|
||||
* are no categories (if selected == null) or the selected element is not
|
||||
* a category (in which case its name is null).
|
||||
*
|
||||
* @return {string} The name of the category currently selected.
|
||||
*/
|
||||
FactoryModel.prototype.getSelectedName = function() {
|
||||
return this.selected ? this.selected.name : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the currently selected list element object.
|
||||
*
|
||||
* @return {ListElement} The currently selected ListElement
|
||||
*/
|
||||
FactoryModel.prototype.getSelected = function() {
|
||||
return this.selected;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets list element currently selected by id.
|
||||
*
|
||||
* @param {string} id ID of list element that should now be selected.
|
||||
*/
|
||||
FactoryModel.prototype.setSelectedById = function(id) {
|
||||
this.selected = this.getElementById(id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an ID of a list element, returns the index of that list element in
|
||||
* toolboxList. Returns -1 if ID is not present.
|
||||
*
|
||||
* @param {!string} id The ID of list element to search for.
|
||||
* @return {int} The index of the list element in toolboxList, or -1 if it
|
||||
* doesn't exist.
|
||||
*/
|
||||
|
||||
FactoryModel.prototype.getIndexByElementId = function(id) {
|
||||
for (var i = 0; i < this.toolboxList.length; i++) {
|
||||
if (this.toolboxList[i].id == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1; // ID not present in toolboxList.
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the ID of a list element, returns that ListElement object.
|
||||
*
|
||||
* @param {!string} id The ID of element to search for.
|
||||
* @return {ListElement} Corresponding ListElement object in toolboxList, or
|
||||
* null if that element does not exist.
|
||||
*/
|
||||
FactoryModel.prototype.getElementById = function(id) {
|
||||
for (var i = 0; i < this.toolboxList.length; i++) {
|
||||
if (this.toolboxList[i].id == id) {
|
||||
return this.toolboxList[i];
|
||||
}
|
||||
}
|
||||
return null; // ID not present in toolboxList.
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the index of a list element in toolboxList, returns that ListElement
|
||||
* object.
|
||||
*
|
||||
* @param {int} index The index of the element to return.
|
||||
* @return {ListElement} The corresponding ListElement object in toolboxList.
|
||||
*/
|
||||
FactoryModel.prototype.getElementByIndex = function(index) {
|
||||
if (index < 0 || index >= this.toolboxList.length) {
|
||||
return null;
|
||||
}
|
||||
return this.toolboxList[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the xml to load the selected element.
|
||||
*
|
||||
* @return {!Element} The XML of the selected element, or null if there is
|
||||
* no selected element.
|
||||
*/
|
||||
FactoryModel.prototype.getSelectedXml = function() {
|
||||
return this.selected ? this.selected.xml : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return ordered list of ListElement objects.
|
||||
*
|
||||
* @return {!Array<!ListElement>} ordered list of ListElement objects
|
||||
*/
|
||||
FactoryModel.prototype.getToolboxList = function() {
|
||||
return this.toolboxList;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the ID of a category given its name.
|
||||
*
|
||||
* @param {string} name Name of category.
|
||||
* @return {int} ID of category
|
||||
*/
|
||||
FactoryModel.prototype.getCategoryIdByName = function(name) {
|
||||
for (var i = 0; i < this.toolboxList.length; i++) {
|
||||
if (this.toolboxList[i].name == name) {
|
||||
return this.toolboxList[i].id;
|
||||
}
|
||||
}
|
||||
return null; // Name not present in toolboxList.
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the toolbox list, deleting all ListElements.
|
||||
*/
|
||||
FactoryModel.prototype.clearToolboxList = function() {
|
||||
this.toolboxList = [];
|
||||
this.hasVariableCategory = false;
|
||||
this.hasVariableCategory = false;
|
||||
// TODO(evd2014): When merge changes, also clear shadowList.
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for a ListElement
|
||||
* Adds a shadow block to the list of shadow blocks.
|
||||
*
|
||||
* @param {!string} blockId The unique ID of block to be added.
|
||||
*/
|
||||
FactoryModel.prototype.addShadowBlock = function(blockId) {
|
||||
this.shadowBlocks.push(blockId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a shadow block ID from the list of shadow block IDs if that ID is
|
||||
* in the list.
|
||||
*
|
||||
* @param {!string} blockId The unique ID of block to be removed.
|
||||
*/
|
||||
FactoryModel.prototype.removeShadowBlock = function(blockId) {
|
||||
for (var i = 0; i < this.shadowBlocks.length; i++) {
|
||||
if (this.shadowBlocks[i] == blockId) {
|
||||
this.shadowBlocks.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a block is a shadow block given a unique block ID.
|
||||
*
|
||||
* @param {!string} blockId The unique ID of the block to examine.
|
||||
* @return {boolean} True if the block is a user-generated shadow block, false
|
||||
* otherwise.
|
||||
*/
|
||||
FactoryModel.prototype.isShadowBlock = function(blockId) {
|
||||
for (var i = 0; i < this.shadowBlocks.length; i++) {
|
||||
if (this.shadowBlocks[i] == blockId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of blocks currently loaded, returns all blocks in the workspace
|
||||
* that are user generated shadow blocks.
|
||||
*
|
||||
* @param {!<Blockly.Block>} blocks Array of blocks currently loaded.
|
||||
* @return {!<Blockly.Block>} Array of user-generated shadow blocks currently
|
||||
* loaded.
|
||||
*/
|
||||
FactoryModel.prototype.getShadowBlocksInWorkspace = function(workspaceBlocks) {
|
||||
var shadowsInWorkspace = [];
|
||||
for (var i = 0; i < workspaceBlocks.length; i++) {
|
||||
if (this.isShadowBlock(workspaceBlocks[i].id)) {
|
||||
shadowsInWorkspace.push(workspaceBlocks[i]);
|
||||
}
|
||||
}
|
||||
return shadowsInWorkspace;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a custom tag to a category, updating state variables accordingly.
|
||||
* Only accepts 'VARIABLE' and 'PROCEDURE' tags.
|
||||
*
|
||||
* @param {!ListElement} category The category to add the tag to.
|
||||
* @param {!string} tag The custom tag to add to the category.
|
||||
*/
|
||||
FactoryModel.prototype.addCustomTag = function(category, tag) {
|
||||
// Only update list elements that are categories.
|
||||
if (category.type != ListElement.TYPE_CATEGORY) {
|
||||
return;
|
||||
}
|
||||
// Only update the tag to be 'VARIABLE' or 'PROCEDURE'.
|
||||
if (tag == 'VARIABLE') {
|
||||
this.hasVariableCategory = true;
|
||||
category.custom = 'VARIABLE';
|
||||
} else if (tag == 'PROCEDURE') {
|
||||
this.hasProcedureCategory = true;
|
||||
category.custom = 'PROCEDURE';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Class for a ListElement.
|
||||
* @constructor
|
||||
*/
|
||||
ListElement = function(type, opt_name) {
|
||||
this.type = type;
|
||||
// XML DOM element to load the element.
|
||||
this.xml = Blockly.Xml.textToDom('<xml></xml>');
|
||||
// Name of category. Can be changed by user. Null if separator.
|
||||
this.name = opt_name ? opt_name : null;
|
||||
// Unique ID of element. Does not change.
|
||||
this.id = Blockly.genUid();
|
||||
// Color of category. Default is no color. Null if separator.
|
||||
this.color = null;
|
||||
// Stores a custom tag, if necessary. Null if no custom tag or separator.
|
||||
this.custom = null;
|
||||
};
|
||||
|
||||
// List element types.
|
||||
ListElement.TYPE_CATEGORY = 'category';
|
||||
ListElement.TYPE_SEPARATOR = 'separator';
|
||||
|
||||
/**
|
||||
* Saves a category by updating its XML (does not save XML for
|
||||
* elements that are not categories).
|
||||
*
|
||||
* @param {!Blockly.workspace} workspace The workspace to save category entry
|
||||
* from.
|
||||
*/
|
||||
ListElement.prototype.saveFromWorkspace = function(workspace) {
|
||||
// Only save list elements that are categories.
|
||||
if (this.type != ListElement.TYPE_CATEGORY) {
|
||||
return;
|
||||
}
|
||||
this.xml = Blockly.Xml.workspaceToDom(workspace);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Changes the name of a category object given a new name. Returns if
|
||||
* not a category.
|
||||
*
|
||||
* @param {string} name New name of category.
|
||||
*/
|
||||
ListElement.prototype.changeName = function (name) {
|
||||
// Only update list elements that are categories.
|
||||
if (this.type != ListElement.TYPE_CATEGORY) {
|
||||
return;
|
||||
}
|
||||
this.name = name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the color of a category. If tries to set the color of something other
|
||||
* than a category, returns.
|
||||
*
|
||||
* @param {!string} color The color that should be used for that category.
|
||||
*/
|
||||
ListElement.prototype.changeColor = function (color) {
|
||||
if (this.type != ListElement.TYPE_CATEGORY) {
|
||||
return;
|
||||
}
|
||||
this.color = color;
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes a copy of the original element and returns it. Everything about the
|
||||
* copy is identical except for its ID.
|
||||
*
|
||||
* @return {!ListElement} The copy of the ListElement.
|
||||
*/
|
||||
ListElement.prototype.copy = function() {
|
||||
copy = new ListElement(this.type);
|
||||
// Generate a unique ID for the element.
|
||||
copy.id = Blockly.genUid();
|
||||
// Copy all attributes except ID.
|
||||
copy.name = this.name;
|
||||
copy.xml = this.xml;
|
||||
copy.color = this.color;
|
||||
copy.custom = this.custom;
|
||||
// Return copy.
|
||||
return copy;
|
||||
};
|
||||
341
demos/blocklyfactory/workspacefactory/wfactory_view.js
Normal file
341
demos/blocklyfactory/workspacefactory/wfactory_view.js
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* @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 FactoryController (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 FactoryView
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
FactoryView = 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
|
||||
*/
|
||||
FactoryView.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) {
|
||||
table.deleteRow(0);
|
||||
}
|
||||
// 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.
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.prototype.addEmptyCategoryMessage = function() {
|
||||
var table = document.getElementById('categoryTable');
|
||||
if (table.rows.length == 0) {
|
||||
var row = table.insertRow(0);
|
||||
row.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.
|
||||
*/
|
||||
FactoryView.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
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.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
|
||||
*/
|
||||
FactoryView.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
|
||||
*/
|
||||
FactoryView.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
|
||||
*
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.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.
|
||||
*/
|
||||
FactoryView.prototype.shouldDisableWorkspace = function(category) {
|
||||
return category != null && (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.
|
||||
*/
|
||||
FactoryView.prototype.clearToolboxTabs = function() {
|
||||
this.tabMap = [];
|
||||
var oldCategoryTable = document.getElementById('categoryTable');
|
||||
var newCategoryTable = document.createElement('table');
|
||||
newCategoryTable.id = 'categoryTable';
|
||||
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.
|
||||
*/
|
||||
FactoryView.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).
|
||||
*/
|
||||
FactoryView.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).
|
||||
*/
|
||||
FactoryView.prototype.unmarkShadowBlock = function(block) {
|
||||
// Remove Blockly CSS for user-generated shadow blocks.
|
||||
if (Blockly.hasClass_(block.svgGroup_, 'shadowBlock')) {
|
||||
Blockly.removeClass_(block.svgGroup_, 'shadowBlock');
|
||||
}
|
||||
};
|
||||
@@ -59,6 +59,29 @@ function test_domToText() {
|
||||
text.replace(/\s+/g, ''));
|
||||
}
|
||||
|
||||
function test_domToWorkspace() {
|
||||
Blockly.Blocks.test_block = {
|
||||
init: function() {
|
||||
this.jsonInit({
|
||||
message0: 'test',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
var dom = Blockly.Xml.textToDom(
|
||||
'<xml xmlns="http://www.w3.org/1999/xhtml">' +
|
||||
' <block type="test_block" inline="true" x="21" y="23">' +
|
||||
' </block>' +
|
||||
'</xml>');
|
||||
var workspace = new Blockly.Workspace();
|
||||
Blockly.Xml.domToWorkspace(dom, workspace);
|
||||
assertEquals('Block count', 1, workspace.getAllBlocks().length);
|
||||
} finally {
|
||||
delete Blockly.Blocks.test_block;
|
||||
}
|
||||
}
|
||||
|
||||
function test_domToPrettyText() {
|
||||
var dom = Blockly.Xml.textToDom(XML_TEXT);
|
||||
var text = Blockly.Xml.domToPrettyText(dom);
|
||||
|
||||
Reference in New Issue
Block a user