Add a contextual modal for the toolbox.

This commit is contained in:
Sean Lip
2016-11-17 15:58:23 -08:00
parent 0e9651ee88
commit 6ab1244560
8 changed files with 331 additions and 36 deletions

View File

@@ -33,6 +33,7 @@ blocklyApp.AppView = ng.core.Component({
</div>
<blockly-block-options-modal></blockly-block-options-modal>
<blockly-toolbox-modal></blockly-toolbox-modal>
<div>
<blockly-toolbox></blockly-toolbox>
@@ -47,7 +48,8 @@ blocklyApp.AppView = ng.core.Component({
`,
directives: [
blocklyApp.ToolboxComponent, blocklyApp.WorkspaceComponent,
blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent],
blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent,
blocklyApp.ToolboxModalComponent],
pipes: [blocklyApp.TranslatePipe],
// All services are declared here, so that all components in the
// application use the same instance of the service.
@@ -56,7 +58,7 @@ blocklyApp.AppView = ng.core.Component({
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
blocklyApp.TreeService, blocklyApp.UtilsService,
blocklyApp.AudioService, blocklyApp.BlockOptionsModalService,
blocklyApp.KeyboardInputService]
blocklyApp.KeyboardInputService, blocklyApp.ToolboxModalService]
})
.Class({
constructor: [

View File

@@ -51,36 +51,7 @@ blocklyApp.BlockOptionsModalComponent = ng.core.Component({
</div>
</div>
`,
pipes: [blocklyApp.TranslatePipe],
styles: [
`.blocklyModalCurtain {
background-color: rgba(0,0,0,0.4);
height: 100%;
left: 0;
overflow: auto;
position: fixed;
top: 0;
width: 100%;
z-index: 1;
}
`, `
.blocklyModal {
background-color: #fefefe;
border: 1px solid #888;
margin: 15% auto;
max-width: 600px;
padding: 20px;
width: 60%;
}
`, `
.blocklyModalButtonContainer {
margin: 10px 0;
}
`, `
.blocklyModal .activeButton {
border: 1px solid blue;
}
`]
pipes: [blocklyApp.TranslatePipe]
})
.Class({
constructor: [

View File

@@ -26,7 +26,11 @@
blocklyApp.BlockOptionsModalService = ng.core.Class({
constructor: [function() {
this.actionButtonsInfo = [];
this.preShowHook = null;
this.preShowHook = function() {
throw Error(
'A pre-show hook must be defined for the block options modal ' +
'before it can be shown.');
};
this.modalIsShown = false;
this.onHideCallback = null;
}],

View File

@@ -10,7 +10,7 @@
float: left;
margin-left: 10px;
margin-top: 20px;
width: 150px;
width: 200px;
}
.blocklyAriaLiveStatus {
@@ -35,3 +35,28 @@
.blocklyDropdownListItem[aria-selected="true"] button {
font-weight: bold;
}
.blocklyModalCurtain {
background-color: rgba(0,0,0,0.4);
height: 100%;
left: 0;
overflow: auto;
position: fixed;
top: 0;
width: 100%;
z-index: 1;
}
.blocklyModal {
background-color: #fefefe;
border: 1px solid #888;
margin: 15% auto;
max-width: 600px;
padding: 20px;
width: 60%;
}
.blocklyModalButtonContainer {
margin: 10px 0;
}
.blocklyModal .activeButton {
border: 1px solid blue;
}

View File

@@ -37,6 +37,10 @@ blocklyApp.SidebarComponent = ng.core.Component({
{{buttonConfig.text}}
</button>
</span>
<button (click)="showToolboxModal()"
class="blocklySidebarButton">
Create new block group...
</button>
<button id="clear-workspace" (click)="workspace.clear()"
[attr.aria-disabled]="isWorkspaceEmpty()"
class="blocklySidebarButton">
@@ -50,8 +54,10 @@ blocklyApp.SidebarComponent = ng.core.Component({
.Class({
constructor: [
blocklyApp.NotificationsService, blocklyApp.TreeService,
blocklyApp.UtilsService,
function(_notificationsService, _treeService, _utilsService) {
blocklyApp.UtilsService, blocklyApp.ToolboxModalService,
function(
_notificationsService, _treeService, _utilsService,
_toolboxModalService) {
// ACCESSIBLE_GLOBALS is a global variable defined by the containing
// page. It should contain a key, customSidebarButtons, describing
// additional buttons that should be displayed after the default ones.
@@ -63,6 +69,7 @@ blocklyApp.SidebarComponent = ng.core.Component({
this.notificationsService = _notificationsService;
this.treeService = _treeService;
this.utilsService = _utilsService;
this.toolboxModalService = _toolboxModalService;
}
],
handleButtonClick: function(buttonConfig) {
@@ -77,5 +84,8 @@ blocklyApp.SidebarComponent = ng.core.Component({
},
isWorkspaceEmpty: function() {
return this.utilsService.isWorkspaceEmpty();
},
showToolboxModal: function() {
this.toolboxModalService.showModal();
}
});

View File

@@ -0,0 +1,197 @@
/**
* 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 Component representing the toolbox modal.
*
* @author sll@google.com (Sean Lip)
*/
blocklyApp.ToolboxModalComponent = ng.core.Component({
selector: 'blockly-toolbox-modal',
template: `
<div *ngIf="modalIsVisible" id="toolboxModal" role="dialog" tabindex="-1">
<div (click)="hideModal()" class="blocklyModalCurtain">
<!-- The $event.stopPropagation() here prevents the modal from
closing when its interior is clicked. -->
<div class="blocklyModal" (click)="$event.stopPropagation()" role="document">
<h3>Select a block for the new group...</h3>
<div *ngFor="#toolboxCategory of toolboxCategories; #categoryIndex=index">
<h4>{{toolboxCategory.categoryName}}</h4>
<div class="blocklyModalButtonContainer"
*ngFor="#block of toolboxCategory.blocks; #blockIndex=index">
<button [id]="getOptionId(getOverallIndex(categoryIndex, blockIndex))"
(click)="selectBlock(categoryIndex, blockIndex); hideModal();"
[ngClass]="{activeButton: activeButtonIndex == getOverallIndex(categoryIndex, blockIndex)}">
{{getBlockDescription(block)}}
</button>
</div>
</div>
<hr>
<div class="blocklyModalButtonContainer">
<button [id]="getCancelOptionId()" (click)="hideModal()"
[ngClass]="{activeButton: activeButtonIndex == totalNumBlocks}">
{{'CANCEL'|translate}}
</button>
</div>
</div>
</div>
</div>
`,
pipes: [blocklyApp.TranslatePipe]
})
.Class({
constructor: [
blocklyApp.ToolboxModalService, blocklyApp.KeyboardInputService,
blocklyApp.AudioService, blocklyApp.UtilsService, blocklyApp.TreeService,
blocklyApp.NotificationsService,
function(
toolboxModalService_, keyboardInputService_,
audioService_, utilsService_, treeService_, notificationsService_) {
this.toolboxModalService = toolboxModalService_;
this.keyboardInputService = keyboardInputService_;
this.audioService = audioService_;
this.utilsService = utilsService_;
this.treeService = treeService_;
this.notificationsService = notificationsService_;
this.modalIsVisible = false;
this.toolboxCategories = [];
this.firstBlockIndexes = [];
this.activeButtonIndex = 0;
this.totalNumBlocks = null;
var that = this;
this.toolboxModalService.registerPreShowHook(
function(toolboxCategories) {
that.modalIsVisible = true;
that.toolboxCategories = toolboxCategories;
var cumulativeIndex = 0;
that.toolboxCategories.forEach(function(category) {
that.firstBlockIndexes.push(cumulativeIndex);
cumulativeIndex += category.blocks.length;
});
that.firstBlockIndexes.push(cumulativeIndex);
that.totalNumBlocks = cumulativeIndex;
that.activeButtonIndex = 0;
that.keyboardInputService.setOverride({
// Tab key: no-op.
'9': function(evt) {
evt.preventDefault();
evt.stopPropagation();
},
// Enter key: selects an action, performs it, and closes the
// modal.
'13': function(evt) {
var button = document.getElementById(
that.getOptionId(that.activeButtonIndex));
for (var i = 0; i < that.toolboxCategories.length; i++) {
if (that.firstBlockIndexes[i + 1] > that.activeButtonIndex) {
var categoryIndex = i;
var blockIndex =
that.activeButtonIndex - that.firstBlockIndexes[i];
that.selectBlock(categoryIndex, blockIndex);
break;
}
}
that.hideModal();
},
// Escape key: closes the modal.
'27': function() {
that.hideModal();
},
// Up key: navigates to the previous item in the list.
'38': function(evt) {
evt.preventDefault();
if (that.activeButtonIndex == 0) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex--;
}
that.focusOnOptionIfPossible(that.activeButtonIndex);
},
// Down key: navigates to the next item in the list.
'40': function(evt) {
evt.preventDefault();
if (that.activeButtonIndex == that.totalNumBlocks) {
that.audioService.playOopsSound();
} else {
that.activeButtonIndex++;
}
that.focusOnOptionIfPossible(that.activeButtonIndex);
}
});
setTimeout(function() {
document.getElementById('toolboxModal').focus();
}, 150);
}
);
}
],
getOverallIndex: function(categoryIndex, blockIndex) {
return this.firstBlockIndexes[categoryIndex] + blockIndex;
},
selectBlock: function(categoryIndex, blockIndex) {
var block = this.toolboxCategories[categoryIndex].blocks[blockIndex];
var blockDescription = this.getBlockDescription(block);
var xml = Blockly.Xml.blockToDom(block);
var newBlockId = Blockly.Xml.domToBlock(blocklyApp.workspace, xml).id;
var that = this;
setTimeout(function() {
that.treeService.focusOnBlock(newBlockId);
that.notificationsService.setStatusMessage(
blockDescription + ' added to workspace. ' +
'Now on added block in workspace.');
});
},
getBlockDescription: function(block) {
return this.utilsService.getBlockDescription(block);
},
// Focuses on the button represented by the given index.
focusOnOptionIfPossible: function(index) {
var button = document.getElementById(this.getOptionId(index));
if (!button.disabled) {
button.focus();
} else {
document.activeElement.blur();
}
},
// Returns the ID for the corresponding option button.
getOptionId: function(index) {
return 'toolbox-modal-option-' + index;
},
// Returns the ID for the "cancel" option button.
getCancelOptionId: function() {
return 'toolbox-modal-option-' + this.totalNumBlocks;
},
// Closes the modal.
hideModal: function() {
this.modalIsVisible = false;
this.keyboardInputService.clearOverride();
this.toolboxModalService.hideModal();
}
});

View File

@@ -0,0 +1,84 @@
/**
* 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 for the toolbox modal.
*
* @author sll@google.com (Sean Lip)
*/
blocklyApp.ToolboxModalService = ng.core.Class({
constructor: [function() {
this.modalIsShown = false;
this.onHideCallback = null;
this.preShowHook = function() {
throw Error(
'A pre-show hook must be defined for the toolbox modal before it ' +
'can be shown.');
};
this.toolboxCategories = [];
// Populate the toolbox categories.
var toolboxXmlElt = document.getElementById('blockly-toolbox-xml');
var toolboxCategoryElts = toolboxXmlElt.getElementsByTagName('category');
if (toolboxCategoryElts.length) {
this.toolboxCategories = Array.from(toolboxCategoryElts).map(
function(categoryElt) {
var workspace = new Blockly.Workspace();
Blockly.Xml.domToWorkspace(categoryElt, workspace);
return {
categoryName: categoryElt.attributes.name.value,
blocks: workspace.topBlocks_
};
}
);
} else {
// If there are no top-level categories, we create a single category
// containing all the top-level blocks.
var workspace = new Blockly.Workspace();
Array.from(xmlToolboxElt.children).forEach(function(topLevelNode) {
Blockly.Xml.domToBlock(workspace, topLevelNode);
});
this.toolboxCategories = [{
categoryName: 'Available Blocks',
blocks: workspace.topBlocks_
}];
}
}],
registerPreShowHook: function(preShowHook) {
this.preShowHook = function() {
preShowHook(this.toolboxCategories);
};
},
isModalShown: function() {
return this.modalIsShown;
},
showModal: function(onHideCallback) {
this.onHideCallback = onHideCallback;
this.preShowHook();
this.modalIsShown = true;
},
hideModal: function() {
this.modalIsShown = false;
if (this.onHideCallback) {
this.onHideCallback();
}
}
});

View File

@@ -21,6 +21,7 @@
<script src="../../accessible/utils.service.js"></script>
<script src="../../accessible/audio.service.js"></script>
<script src="../../accessible/block-options-modal.service.js"></script>
<script src="../../accessible/toolbox-modal.service.js"></script>
<script src="../../accessible/keyboard-input.service.js"></script>
<script src="../../accessible/notifications.service.js"></script>
<script src="../../accessible/clipboard.service.js"></script>
@@ -29,6 +30,7 @@
<script src="../../accessible/field-segment.component.js"></script>
<script src="../../accessible/block-options-modal.component.js"></script>
<script src="../../accessible/toolbox-modal.component.js"></script>
<script src="../../accessible/toolbox-tree.component.js"></script>
<script src="../../accessible/toolbox.component.js"></script>
<script src="../../accessible/sidebar.component.js"></script>