diff --git a/accessible/README b/accessible/README index 088905570..21c2e2025 100644 --- a/accessible/README +++ b/accessible/README @@ -1,19 +1,52 @@ Accessible Blockly -======= +================== -Google's Blockly is a web-based, visual programming editor: accessible to blind users. +Google's Blockly is a web-based, visual programming editor that is accessible +to blind users. -What Does Accessible Blockly Do? +The code in this directory renders a version of the Blockly toolbox and +workspace that is fully keyboard-navigable, and compatible with JAWS/NVDA +screen readers on Firefox for Windows. (We chose this combination because JAWS +and NVDA are the most robust screen readers, and are compatible with many more +aria tags than other screen readers.) + +In the future, Accessible Blockly may be modified to suit accessibility needs +other than visual impairments. Note that deaf users are expected to continue +using Blockly over Accessible Blockly. + + +Using Accessible Blockly in Your Web App +---------------------------------------- +The demo at blockly/demos/accessible covers the absolute minimum required to +import Accessible Blockly into your web app. You will need to import the files +in the same order as in the demo: utils.service.js will need to be the first +Angular file imported. + +When the DOMContentLoaded event fires, call ng.platform.browser.bootstrap() on +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 +----------------------- +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: [] + }; + +The value corresponding to 'toolbarButtonConfig' can be modified by adding +objects representing buttons on the toolbar. Each of these objects should have +two keys: + + - 'text' (the text to display on the button) + - 'action' (the function that gets run when the button is clicked) + + +Limitations ----------- -* renders a version of the blockly toolbox and workspace that is easily navigable using solely the keyboard -* renders a version of the blockly toolbox and workspace that is compatible with JAWS/NVDA screen readers on Firefox for Windows. - * Accessible Blockly will hopefully be modified to work well with most other screen readers, however JAWS and NVDA are the most robust screen readers and are compatible with many more aria tags than other screen readers. Therefore it was easiest to first be compatible with them. -* Accessible Blockly will be modified to suit accessibility needs other than visual impairments as well. Deaf users will be expected to continue using Blockly over Accessible Blockly. - -Use Accessible Blockly in Your Web App ------------ -1. See the basic demo under blockly/demos/accessible. This covers the absolute minimum required to import Accessible Blockly into your own web app. -2. You will need to import the files in the same order as in the demo: utils.service.js will need to be the first Angular file imported. -3. When the DOMContentLoaded event fires, call ng.platform.browser.bootstrap() on 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. -4. If you want to customize the toolbar, you will need an ACCESSIBLE_GLOBALS object in the global scope. It should have a 'toolbarButtonConfig' key, whose value is an Array. Each element in this array should be an Object with two keys: 'text' (the text to display on the button) and 'action' (the function that gets run when the button is clicked). -5. Note that we do not support having multiple Accessible Blockly apps in a single webpage. +- We do not support having multiple Accessible Blockly apps in a single webpage. +- Accessible Blockly does not support the use of shadow blocks. diff --git a/accessible/app.component.js b/accessible/app.component.js new file mode 100644 index 000000000..2a8b75218 --- /dev/null +++ b/accessible/app.component.js @@ -0,0 +1,65 @@ +/** + * 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 that details how the AccessibleBlockly + * app is rendered on the page. + * @author madeeha@google.com (Madeeha Ghori) + */ + +blocklyApp.workspace = new Blockly.Workspace(); + +// If the debug flag is true, print console.logs to help with debugging. +blocklyApp.debug = false; + +blocklyApp.AppView = ng.core + .Component({ + selector: 'blockly-app', + template: ` + + + + + +
+ {{'TOOLBOX_LOAD'|translate}} + + {{'WORKSPACE_LOAD'|translate}} +
+ + + + + + + + + + + `, + directives: [blocklyApp.ToolboxComponent, blocklyApp.WorkspaceComponent], + pipes: [blocklyApp.TranslatePipe], + // The clipboard and utils services are declared here, so that all + // components in the application use the same instance of the service. + // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ + providers: [blocklyApp.ClipboardService, blocklyApp.UtilsService] + }) + .Class({ + constructor: function() {} + }); diff --git a/accessible/appview.component.js b/accessible/appview.component.js deleted file mode 100644 index aa8e620f3..000000000 --- a/accessible/appview.component.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 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 that details how the AccessibleBlockly - * app is rendered on the page. - * @author madeeha@google.com (Madeeha Ghori) - */ - -blocklyApp.workspace = new Blockly.Workspace(); - -// If the debug flag is true, print console.logs to help with debugging. -blocklyApp.debug = false; - -blocklyApp.AppView = ng.core - .Component({ - selector: 'blockly-app', - template: ` - - - - - -
- {{stringMap['TOOLBOX_LOAD']}} - - {{stringMap['WORKSPACE_LOAD']}} -
- - - - - - - - - - `, - directives: [blocklyApp.ToolboxView, blocklyApp.WorkspaceView], - // ClipboardService declared here so that all components are using the same - // instance of the clipboard. - // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ - providers: [blocklyApp.ClipboardService] - }) - .Class({ - constructor: function() { - this.stringMap = { - ['TOOLBOX_LOAD']: Blockly.Msg.TOOLBOX_LOAD_MSG, - ['WORKSPACE_LOAD']: Blockly.Msg.WORKSPACE_LOAD_MSG, - ['BLOCK_SUMMARY']: Blockly.Msg.BLOCK_SUMMARY, - ['BLOCK_ACTION_LIST']: Blockly.Msg.BLOCK_ACTION_LIST, - ['OPTION_LIST']: Blockly.Msg.OPTION_LIST, - ['ARGUMENT_OPTIONS_LIST']: Blockly.Msg.ARGUMENT_OPTIONS_LIST, - ['UNAVAILABLE']: Blockly.Msg.UNAVAILABLE, - ['BUTTON']: Blockly.Msg.BUTTON, - ['TEXT']: Blockly.Msg.TEXT, - ['ARGUMENT_BLOCK_ACTION_LIST']: Blockly.Msg.ARGUMENT_BLOCK_ACTION_LIST, - ['ARGUMENT_INPUT']: Blockly.Msg.ARGUMENT_INPUT - }; - } - }); diff --git a/accessible/clipboard.service.js b/accessible/clipboard.service.js index 2caf8d627..dee70dfc0 100644 --- a/accessible/clipboard.service.js +++ b/accessible/clipboard.service.js @@ -25,6 +25,7 @@ blocklyApp.ClipboardService = ng.core .Class({ constructor: function() { + blocklyApp.debug && console.log('Clipboard service constructed'); this.clipboardBlockXml_ = null; this.clipboardBlockSuperiorConnection_ = null; this.clipboardBlockNextConnection_ = null; @@ -61,7 +62,9 @@ blocklyApp.ClipboardService = ng.core connection.connect(reconstitutedBlock.outputConnection); } blocklyApp.debug && console.log('paste'); - alert(Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG + block.toString()); + alert( + Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG + + reconstitutedBlock.toString()); }, pasteToMarkedConnection: function(block, announce) { var xml = Blockly.Xml.blockToDom(block); @@ -72,7 +75,9 @@ blocklyApp.ClipboardService = ng.core reconstitutedBlock.previousConnection); blocklyApp.debug && console.log('paste to marked connection'); if (announce) { - alert(Blockly.Msg.PASTED_BLOCK_TO_MARKED_SPOT_MSG + block.toString()); + alert( + Blockly.Msg.PASTED_BLOCK_TO_MARKED_SPOT_MSG + + reconstitutedBlock.toString()); } }, markConnection: function(connection) { @@ -81,39 +86,26 @@ blocklyApp.ClipboardService = ng.core alert(Blockly.Msg.MARKED_SPOT_MSG); }, isCompatibleWithConnection_: function(blockConnection, connection) { - // Checking that the connection and blockConnection exist. - if (!connection || !blockConnection) { - return false; - } - - // Checking that the types match and it's the right kind of connection. - var isCompatible = Blockly.OPPOSITE_TYPE[blockConnection.type] == - connection.type && connection.checkType_(blockConnection); - - if (blocklyApp.debug) { - if (isCompatible) { - console.log('blocks should be connected'); - } else { - console.log('blocks should not be connected'); - } - } - return isCompatible; + // Check that both connections exist, that the types match, and that it's + // the right kind of connection. + return Boolean( + connection && blockConnection && + Blockly.OPPOSITE_TYPE[blockConnection.type] == connection.type && + connection.checkType_(blockConnection)); }, isBlockCompatibleWithMarkedConnection: function(block) { var blockConnection = block.outputConnection || block.previousConnection; - return this.markedConnection_ && + return Boolean( + this.markedConnection_ && this.markedConnection_.sourceBlock_.workspace && this.isCompatibleWithConnection_( - blockConnection, this.markedConnection_); + blockConnection, this.markedConnection_)); }, - getClipboardCompatibilityHTMLText: function(connection) { - if (this.isCompatibleWithConnection_(connection, - this.clipboardBlockSuperiorConnection_) || - this.isCompatibleWithConnection_(connection, - this.clipboardBlockNextConnection_)){ - return ''; - } else { - return 'blockly-disabled'; - } + isClipboardCompatibleWithConnection: function(connection) { + var superiorConnection = this.clipboardBlockSuperiorConnection_; + var nextConnection = this.clipboardBlockNextConnection_; + return Boolean( + this.isCompatibleWithConnection_(connection, superiorConnection) || + this.isCompatibleWithConnection_(connection, nextConnection)); } }); diff --git a/accessible/fieldview.component.js b/accessible/field.component.js similarity index 80% rename from accessible/fieldview.component.js rename to accessible/field.component.js index 7e68cd9d7..6f5f06284 100644 --- a/accessible/fieldview.component.js +++ b/accessible/field.component.js @@ -24,19 +24,19 @@ * @author madeeha@google.com (Madeeha Ghori) */ -blocklyApp.FieldView = ng.core +blocklyApp.FieldComponent = ng.core .Component({ - selector: 'field-view', + selector: 'blockly-field', template: `
  • + [attr.aria-level]="level" aria-selected=false>
  • -
  • - + [attr.aria-level]="level" aria-selected=false> +
  • -
  • - // Checkboxes not currently supported. +
  • + // Checkboxes are not currently supported.
  • -
  • `, inputs: ['field', 'level', 'index', 'parentId'], - providers: [blocklyApp.TreeService, blocklyApp.UtilsService] + pipes: [blocklyApp.TranslatePipe] }) .Class({ - constructor: [blocklyApp.TreeService, blocklyApp.UtilsService, - function(_treeService, _utilsService) { + constructor: [blocklyApp.UtilsService, function(_utilsService) { this.optionText = { keys: [] }; - this.treeService = _treeService; this.utilsService = _utilsService; - this.stringMap = { - 'CURRENT_ARGUMENT_VALUE': Blockly.Msg.CURRENT_ARGUMENT_VALUE - }; }], ngOnInit: function() { var elementsNeedingIds = this.generateElementNames(this.field); @@ -82,9 +75,8 @@ blocklyApp.FieldView = ng.core // this.generateElementNames() are unique. this.idMap = this.utilsService.generateIds(elementsNeedingIds); }, - generateAriaLabelledByAttr: function() { - return this.utilsService.generateAriaLabelledByAttr.apply(this, - arguments); + generateAriaLabelledByAttr: function(mainLabel, secondLabel) { + return mainLabel + ' ' + secondLabel; }, generateElementNames: function() { var elementNames = ['listItem']; diff --git a/accessible/toolbox-tree.component.js b/accessible/toolbox-tree.component.js new file mode 100644 index 000000000..364cf586e --- /dev/null +++ b/accessible/toolbox-tree.component.js @@ -0,0 +1,167 @@ +/** + * 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 that details how blocks are + * rendered in the toolbox in AccessibleBlockly. Also handles any interactions + * with the blocks. + * @author madeeha@google.com (Madeeha Ghori) + */ + +blocklyApp.ToolboxTreeComponent = ng.core + .Component({ + selector: 'blockly-toolbox-tree', + template: ` +
  • + +
      +
    1. + +
        +
      1. + +
      2. +
      3. + +
      4. +
      5. + +
      6. +
      +
    2. +
      + + + + +
    3. + + +
    4. +
      +
    +
  • + + + `, + directives: [blocklyApp.FieldComponent, ng.core.forwardRef(function() { + return blocklyApp.ToolboxTreeComponent; + })], + inputs: [ + 'block', 'displayBlockMenu', 'level', 'index', 'tree', 'noCategories'], + pipes: [blocklyApp.TranslatePipe] + }) + .Class({ + constructor: [ + blocklyApp.ClipboardService, blocklyApp.TreeService, blocklyApp.UtilsService, + function(_clipboardService, _treeService, _utilsService) { + // ClipboardService and UtilsService are app-wide singleton services. + // TreeService is from the parent ToolboxComponent. + this.infoBlocks = Object.create(null); + this.clipboardService = _clipboardService; + this.treeService = _treeService; + this.utilsService = _utilsService; + }], + ngOnInit: function() { + var elementsNeedingIds = ['blockSummaryLabel']; + if (this.displayBlockMenu || this.block.inputList.length){ + elementsNeedingIds = elementsNeedingIds.concat(['listItem', 'label', + 'workspaceCopy', 'workspaceCopyButton', 'blockCopy', + 'blockCopyButton', 'sendToSelected', 'sendToSelectedButton']); + } + for (var i = 0; i < this.block.inputList.length; i++){ + elementsNeedingIds.push('listItem' + i, 'listItem' + i + 'Label') + } + this.idMap = this.utilsService.generateIds(elementsNeedingIds); + if (this.index == 0 && this.noCategories) { + this.idMap['parentList'] = 'blockly-toolbox-tree-node0'; + } else { + this.idMap['parentList'] = this.utilsService.generateUniqueId(); + } + }, + ngAfterViewInit: function() { + // If this is a top-level tree in the toolbox, set its active + // descendant after the ids have been computed. + if (this.index == 0 && + this.tree.getAttribute('aria-activedescendant') == + 'blockly-toolbox-tree-node0') { + this.treeService.setActiveDesc( + document.getElementById(this.idMap['parentList']), + this.tree); + } + }, + generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) { + return this.utilsService.generateAriaLabelledByAttr( + mainLabel, secondLabel, isDisabled); + }, + copyToWorkspace: function(block) { + var xml = Blockly.Xml.blockToDom(block); + Blockly.Xml.domToBlock(blocklyApp.workspace, xml); + alert('Added block to workspace: ' + block.toString()); + }, + copyToClipboard: function(block) { + if (this.clipboardService) { + this.clipboardService.copy(block, true); + } + }, + copyToMarked: function(block) { + if (this.clipboardService) { + this.clipboardService.pasteToMarkedConnection(block); + } + } + }); diff --git a/accessible/toolboxview.component.js b/accessible/toolbox.component.js similarity index 82% rename from accessible/toolboxview.component.js rename to accessible/toolbox.component.js index bb33692e3..f6e8009d1 100644 --- a/accessible/toolboxview.component.js +++ b/accessible/toolbox.component.js @@ -23,9 +23,9 @@ * @author madeeha@google.com (Madeeha Ghori) */ -blocklyApp.ToolboxView = ng.core +blocklyApp.ToolboxComponent = ng.core .Component({ - selector: 'toolbox-view', + selector: 'blockly-toolbox', template: `

    Toolbox

      {{labelCategory(name, i, tree)}}
        - - + +
      - - + +
    `, - directives: [blocklyApp.ToolboxTreeView], - providers: [blocklyApp.TreeService, blocklyApp.UtilsService], + directives: [blocklyApp.ToolboxTreeComponent], + providers: [blocklyApp.TreeService], }) .Class({ constructor: [ @@ -111,8 +111,7 @@ blocklyApp.ToolboxView = ng.core parent.id = 'blockly-toolbox-tree-node' + i; if (i == 0 && tree.getAttribute('aria-activedescendant') == 'blockly-toolbox-tree-node0') { - this.treeService.setActiveDesc(parent, tree.id); - parent.setAttribute('aria-selected', 'true'); + this.treeService.setActiveDesc(parent, tree); } }, getToolboxWorkspace: function(categoryNode) { diff --git a/accessible/toolbox_treeview.component.js b/accessible/toolbox_treeview.component.js deleted file mode 100644 index dca39da82..000000000 --- a/accessible/toolbox_treeview.component.js +++ /dev/null @@ -1,175 +0,0 @@ -/** - * 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 that details how blocks are - * rendered in the toolbox in AccessibleBlockly. Also handles any interactions - * with the blocks. - * @author madeeha@google.com (Madeeha Ghori) - */ - -blocklyApp.ToolboxTreeView = ng.core - .Component({ - selector: 'toolbox-tree-view', - template: ` -
  • - {{setActiveDesc(parentList)}} - -
      -
    1. - -
        -
      1. - -
      2. -
      3. - -
      4. -
      5. - -
      6. -
      -
    2. -
      - - - - -
    3. - - -
    4. -
      -
    -
  • - - - `, - directives: [ng.core.forwardRef( - function() { return blocklyApp.ToolboxTreeView; }), - blocklyApp.FieldView], - inputs: ['block', 'displayBlockMenu', 'level', 'index', 'tree', - 'noCategories'], - providers: [blocklyApp.TreeService, blocklyApp.UtilsService] - }) - .Class({ - constructor: [blocklyApp.ClipboardService, blocklyApp.TreeService, - blocklyApp.UtilsService, - function(_clipboardService, _treeService, _utilsService) { - this.infoBlocks = Object.create(null); - this.clipboardService = _clipboardService; - this.treeService = _treeService; - this.utilsService = _utilsService; - this.stringMap = { - 'BLOCK_ACTION_LIST': Blockly.Msg.BLOCK_ACTION_LIST, - 'COPY_TO_CLIPBOARD': Blockly.Msg.COPY_TO_CLIPBOARD, - 'COPY_TO_WORKSPACE': Blockly.Msg.COPY_TO_WORKSPACE, - 'COPY_TO_MARKED_SPOT': Blockly.Msg.COPY_TO_MARKED_SPOT - }; - }], - ngOnInit: function() { - var elementsNeedingIds = ['blockSummaryLabel']; - if (this.displayBlockMenu || this.block.inputList.length){ - elementsNeedingIds = elementsNeedingIds.concat(['listItem', 'label', - 'workspaceCopy', 'workspaceCopyButton', 'blockCopy', - 'blockCopyButton', 'sendToSelected', 'sendToSelectedButton']); - } - for (var i = 0; i < this.block.inputList.length; i++){ - elementsNeedingIds.push('listItem' + i); - elementsNeedingIds.push('listItem' + i + 'Label') - } - this.idMap = this.utilsService.generateIds(elementsNeedingIds); - if (this.index == 0 && this.noCategories) { - this.idMap['parentList'] = 'blockly-toolbox-tree-node0'; - } else { - this.idMap['parentList'] = this.utilsService.generateUniqueId(); - } - }, - getMarkedBlockCompatibilityHTMLText: function(isCompatible) { - return this.utilsService.getMarkedBlockCompatibilityHTMLText( - isCompatible); - }, - generateAriaLabelledByAttr: function() { - return this.utilsService.generateAriaLabelledByAttr.apply(this, - arguments); - }, - setActiveDesc: function(parentList) { - // If this is the first child of the toolbox and the - // current active descendant of the tree is this child, - // then set the active descendant stored in the treeService. - if (this.index == 0 && this.tree.getAttribute('aria-activedescendant') == - 'blockly-toolbox-tree-node0') { - this.treeService.setActiveDesc(parentList, this.tree.id); - } - }, - copyToWorkspace: function(block) { - var xml = Blockly.Xml.blockToDom(block); - Blockly.Xml.domToBlock(blocklyApp.workspace, xml); - alert('Added block to workspace: ' + block.toString()); - }, - copyToClipboard: function(block) { - if (this.clipboardService) { - this.clipboardService.copy(block, true); - } - }, - copyToMarked: function(block) { - if (this.clipboardService) { - this.clipboardService.pasteToMarkedConnection(block); - } - } - }); diff --git a/accessible/translate.pipe.js b/accessible/translate.pipe.js new file mode 100644 index 000000000..c15dec898 --- /dev/null +++ b/accessible/translate.pipe.js @@ -0,0 +1,34 @@ +/** + * 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 Pipe for internationalizing Blockly message strings. + * @author sll@google.com (Sean Lip) + */ + +blocklyApp.TranslatePipe = ng.core + .Pipe({ + name: 'translate' + }) + .Class({ + constructor: function() {}, + transform: function(messageId) { + return Blockly.Msg[messageId]; + } + }); diff --git a/accessible/tree.service.js b/accessible/tree.service.js index 27c38ce02..27f5aebfe 100644 --- a/accessible/tree.service.js +++ b/accessible/tree.service.js @@ -19,6 +19,8 @@ /** * @fileoverview Angular2 Service that handles all tree keyboard navigation. + * A separate TreeService is constructed for each tree in the application. + * * @author madeeha@google.com (Madeeha Ghori) */ @@ -26,8 +28,6 @@ blocklyApp.TreeService = ng.core .Class({ constructor: function() { blocklyApp.debug && console.log('making a new tree service'); - // Keeping track of the active descendants in each tree. - this.activeDesc_ = Object.create(null); this.trees = document.getElementsByClassName('blocklyTree'); // Keeping track of the last key pressed. If the user presses // enter (to edit a text input or press a button), the keyboard @@ -36,43 +36,31 @@ blocklyApp.TreeService = ng.core // to shift focus back to the tree as a whole. this.previousKey_ = null; }, - createId: function(obj) { - if (obj && obj.id) { - return obj.id; - } - return 'blockly-' + Blockly.genUid(); - }, - setActiveDesc: function(node, id) { - blocklyApp.debug && console.log('setting active descendant for tree ' + id); - this.activeDesc_[id] = node; - }, - getActiveDesc: function(id) { - return this.activeDesc_[id] || - document.getElementById((document.getElementById(id)).getAttribute('aria-activedescendant')); - }, - // Makes a given node the active descendant of a given tree. - updateSelectedNode: function(node, tree, keepFocus) { - blocklyApp.debug && console.log('updating node: ' + node.id); - var treeId = tree.id; - var activeDesc = this.getActiveDesc(treeId); + // Make a given node the active descendant of a given tree. + setActiveDesc: function(node, tree, keepFocus) { + blocklyApp.debug && console.log('setting activeDesc for tree ' + tree.id); + + var activeDesc = this.getActiveDesc(tree.id); if (activeDesc) { activeDesc.classList.remove('blocklyActiveDescendant'); activeDesc.setAttribute('aria-selected', 'false'); - } else { - blocklyApp.debug && console.log('updateSelectedNode: there is no active descendant'); } - node.classList.add('blocklyActiveDescendant'); - tree.setAttribute('aria-activedescendant', node.id); - this.setActiveDesc(node, treeId); - node.setAttribute('aria-selected', 'true'); - // Make sure keyboard focus is on tree as a whole - // in case focus was previously on a button or input - // element. + node.classList.add('blocklyActiveDescendant'); + node.setAttribute('aria-selected', 'true'); + tree.setAttribute('aria-activedescendant', node.id); + + // Make sure keyboard focus is on the entire tree in the case where the + // focus was previously on a button or input element. if (keepFocus) { tree.focus(); } }, + getActiveDesc: function(treeId) { + var activeDescendantId = document.getElementById( + treeId).getAttribute('aria-activedescendant'); + return document.getElementById(activeDescendantId); + }, onWorkspaceToolbarKeypress: function(e, treeId) { blocklyApp.debug && console.log(e.keyCode + 'inside TreeService onWorkspaceToolbarKeypress'); switch (e.keyCode) { @@ -155,7 +143,7 @@ blocklyApp.TreeService = ng.core if (!nextNode || nextNode.className == 'treeview') { return; } - this.updateSelectedNode(nextNode, tree, keepFocus); + this.setActiveDesc(nextNode, tree, keepFocus); this.previousKey_ = e.keyCode; e.preventDefault(); e.stopPropagation(); @@ -165,7 +153,7 @@ blocklyApp.TreeService = ng.core blocklyApp.debug && console.log('node passed in: ' + node.id); var prevSibling = this.getPreviousSibling(node); if (prevSibling && prevSibling.tagName != 'H1') { - this.updateSelectedNode(prevSibling, tree, keepFocus); + this.setActiveDesc(prevSibling, tree, keepFocus); } else { blocklyApp.debug && console.log('no previous sibling'); } @@ -177,7 +165,7 @@ blocklyApp.TreeService = ng.core blocklyApp.debug && console.log('in right arrow section'); var firstChild = this.getFirstChild(node); if (firstChild) { - this.updateSelectedNode(firstChild, tree, keepFocus); + this.setActiveDesc(firstChild, tree, keepFocus); } else { blocklyApp.debug && console.log('no valid child'); } @@ -191,7 +179,7 @@ blocklyApp.TreeService = ng.core blocklyApp.debug && console.log('preventing propogation'); var nextSibling = this.getNextSibling(node); if (nextSibling) { - this.updateSelectedNode(nextSibling, tree, keepFocus); + this.setActiveDesc(nextSibling, tree, keepFocus); } else { blocklyApp.debug && console.log('no next sibling'); } diff --git a/accessible/utils.service.js b/accessible/utils.service.js index fddb47ded..7a1e0d273 100644 --- a/accessible/utils.service.js +++ b/accessible/utils.service.js @@ -18,8 +18,10 @@ */ /** - * @fileoverview Angular2 Service with functions required by multiple - * components. + * @fileoverview Angular2 utility service for multiple components. All + * functions in this service should be stateless, since this is a singleton + * service that is used for the entire application. + * * @author madeeha@google.com (Madeeha Ghori) */ @@ -28,6 +30,7 @@ var blocklyApp = {}; blocklyApp.UtilsService = ng.core .Class({ constructor: function() { + blocklyApp.debug && console.log('Utils service constructed'); }, generateUniqueId: function() { return 'blockly-' + Blockly.genUid(); @@ -39,8 +42,12 @@ blocklyApp.UtilsService = ng.core } return idMap; }, - generateAriaLabelledByAttr: function() { - return Array.from(arguments).join(' ').trim(); + generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) { + var attrValue = mainLabel + ' ' + secondLabel; + if (isDisabled) { + attrValue += ' blockly-disabled'; + } + return attrValue; }, getInputTypeLabel: function(connection) { // Returns an upper case string in the case of official input type names. @@ -60,16 +67,5 @@ blocklyApp.UtilsService = ng.core } else { return Blockly.Msg.VALUE; } - }, - getMarkedBlockCompatibilityHTMLText: function(isCompatible) { - if (isCompatible) { - // undefined will result in the - // 'copy to marked block' option being ENABLED. - return ''; - } else { - // Anything will result in the - // 'copy to marked block' option being DISABLED. - return 'blockly-disabled'; - } } }); diff --git a/accessible/workspace-tree.component.js b/accessible/workspace-tree.component.js new file mode 100644 index 000000000..25cccc9b1 --- /dev/null +++ b/accessible/workspace-tree.component.js @@ -0,0 +1,211 @@ +/** + * 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 that details how Blockly.Block's are + * rendered in the workspace in AccessibleBlockly. Also handles any + * interactions with the blocks. + * @author madeeha@google.com (Madeeha Ghori) + */ + +blocklyApp.WorkspaceTreeComponent = ng.core + .Component({ + selector: 'blockly-workspace-tree', + template: ` +
  • + +
      +
    1. + +
        +
      1. + +
      2. +
      3. + +
      4. +
      5. + +
      6. +
      7. + +
      8. +
      9. + +
      10. +
      11. + +
      12. +
      13. + +
      14. +
      15. + +
      16. +
      +
    2. +
      + + + +
    3. + + +
        +
      1. + +
      2. +
      3. + +
      4. +
      +
    4. +
      +
    +
  • + + + + `, + directives: [blocklyApp.FieldComponent, ng.core.forwardRef(function() { + return blocklyApp.WorkspaceTreeComponent; + })], + inputs: ['block', 'isTopBlock', 'topBlockIndex', 'level', 'parentId', 'tree'], + pipes: [blocklyApp.TranslatePipe], + providers: [blocklyApp.TreeService], + }) + .Class({ + constructor: [ + blocklyApp.ClipboardService, blocklyApp.TreeService, blocklyApp.UtilsService, + function(_clipboardService, _treeService, _utilsService) { + this.infoBlocks = Object.create(null); + this.clipboardService = _clipboardService; + this.treeService = _treeService; + this.utilsService = _utilsService; + }], + ngOnInit: function() { + var elementsNeedingIds = ['blockSummary', 'listItem', 'label', + 'cutListItem', 'cutButton', 'copyListItem', 'copyButton', + 'pasteBelow', 'pasteBelowButton', 'pasteAbove', 'pasteAboveButton', + 'markBelow', 'markBelowButton', 'markAbove', 'markAboveButton', + 'sendToSelectedListItem', 'sendToSelectedButton', 'delete', + 'deleteButton']; + for (var i = 0; i < this.block.inputList.length; i++) { + var inputBlock = this.block.inputList[i]; + if (inputBlock.connection && !inputBlock.connection.targetBlock()) { + elementsNeedingIds = elementsNeedingIds.concat( + ['inputList' + i, 'inputMenuLabel' + i, 'markSpot' + i, + 'markSpotButton' + i, 'paste' + i, 'pasteButton' + i]); + } + } + this.idMap = this.utilsService.generateIds(elementsNeedingIds); + this.idMap['parentList'] = + this.isTopBlock ? this.parentId + '-node0' : + this.utilsService.generateUniqueId(); + }, + ngAfterViewInit: function() { + // If this is a top-level tree in the workspace, set its active + // descendant after the ids have been computed. + if (this.tree && + this.tree.getAttribute('aria-activedescendant') == + this.idMap['parentList']) { + this.treeService.setActiveDesc( + document.getElementById(this.idMap['parentList']), + this.tree); + } + }, + isCompatibleWithClipboard: function(connection) { + return this.clipboardService.isClipboardCompatibleWithConnection( + connection); + }, + deleteBlock: function(block) { + // If this is the top block, shift focus to the previous tree. + var topBlocks = blocklyApp.workspace.topBlocks_; + for (var i = 0; i < topBlocks.length; i++) { + if (topBlocks[i].id == block.id) { + this.treeService.goToPreviousTree(this.parentId); + break; + } + } + // If this is not the top block, change the active descendant of the tree. + block.dispose(true); + }, + generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) { + return this.utilsService.generateAriaLabelledByAttr( + mainLabel, secondLabel, isDisabled); + }, + hasPreviousConnection: function(block) { + return Boolean(block.previousConnection); + }, + hasNextConnection: function(block) { + return Boolean(block.nextConnection); + }, + sendToSelected: function(block) { + if (this.clipboardService) { + this.clipboardService.pasteToMarkedConnection(block, false); + block.dispose(true); + alert('Block moved to marked spot: ' + block.toString()); + } + } + }); diff --git a/accessible/workspace.component.js b/accessible/workspace.component.js new file mode 100644 index 000000000..c00cf7e5f --- /dev/null +++ b/accessible/workspace.component.js @@ -0,0 +1,85 @@ +/** + * 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 that details how a Blockly.Workspace is + * rendered in AccessibleBlockly. + * @author madeeha@google.com (Madeeha Ghori) + */ + +blocklyApp.WorkspaceComponent = ng.core + .Component({ + selector: 'blockly-workspace', + template: ` + + +
    + + + + +
    + +
    +
      + + +
    +
    + `, + directives: [blocklyApp.WorkspaceTreeComponent], + pipes: [blocklyApp.TranslatePipe], + providers: [blocklyApp.TreeService] + }) + .Class({ + constructor: [blocklyApp.TreeService, function(_treeService) { + // ACCESSIBLE_GLOBALS is a global variable defined by the containing + // page. It should contain a key, toolbarButtonConfig, whose + // corresponding value is an Array with two keys: 'text' and 'action'. + // The first is the text to display on the button, and the second is the + // function that gets run when the button is clicked. + this.toolbarButtonConfig = + ACCESSIBLE_GLOBALS && ACCESSIBLE_GLOBALS.toolbarButtonConfig ? + ACCESSIBLE_GLOBALS.toolbarButtonConfig : []; + this.workspace = blocklyApp.workspace; + this.treeService = _treeService; + }], + onWorkspaceToolbarKeypress: function(event) { + var activeElementId = document.activeElement.id; + this.treeService.onWorkspaceToolbarKeypress(event, activeElementId); + }, + onKeypress: function(event, tree){ + this.treeService.onKeypress(event, tree); + }, + isWorkspaceEmpty: function() { + return !blocklyApp.workspace.topBlocks_.length; + } + }); diff --git a/accessible/workspace_treeview.component.js b/accessible/workspace_treeview.component.js deleted file mode 100644 index db0001320..000000000 --- a/accessible/workspace_treeview.component.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * 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 that details how Blockly.Block's are - * rendered in the workspace in AccessibleBlockly. Also handles any - * interactions with the blocks. - * @author madeeha@google.com (Madeeha Ghori) - */ - -blocklyApp.WorkspaceTreeView = ng.core - .Component({ - selector: 'tree-view', - template: ` -
  • - {{checkParentList(parentList)}} - -
      -
    1. - -
        -
      1. - -
      2. -
      3. - -
      4. -
      5. - -
      6. -
      7. - -
      8. -
      9. - -
      10. -
      11. - -
      12. -
      13. - -
      14. -
      15. - -
      16. -
      -
    2. -
      - - -
    3. - - -
        -
      1. - -
      2. -
      3. - -
      4. -
      -
    4. -
      -
    -
  • - - - `, - directives: [ng.core.forwardRef( - function() { return blocklyApp.WorkspaceTreeView; }), blocklyApp.FieldView], - inputs: ['block', 'isTopBlock', 'topBlockIndex', 'level', 'parentId'], - providers: [blocklyApp.TreeService, blocklyApp.UtilsService], - }) - .Class({ - constructor: [blocklyApp.ClipboardService, blocklyApp.TreeService, - blocklyApp.UtilsService, - function(_clipboardService, _treeService, _utilsService) { - this.infoBlocks = Object.create(null); - this.clipboardService = _clipboardService; - this.treeService = _treeService; - this.utilsService = _utilsService; - this.stringMap = { - 'BLOCK_ACTION_LIST': Blockly.Msg.BLOCK_ACTION_LIST, - 'PASTE': Blockly.Msg.PASTE, - 'PASTE_ABOVE': Blockly.Msg.PASTE_ABOVE, - 'PASTE_BELOW': Blockly.Msg.PASTE_BELOW, - 'MARK_THIS_SPOT': Blockly.Msg.MARK_THIS_SPOT, - 'MARK_SPOT_ABOVE': Blockly.Msg.MARK_SPOT_ABOVE, - 'MARK_SPOT_BELOW': Blockly.Msg.MARK_SPOT_BELOW, - 'CUT_BLOCK': Blockly.Msg.CUT_BLOCK, - 'COPY_BLOCK': Blockly.Msg.COPY_BLOCK, - 'MOVE_TO_MARKED_SPOT': Blockly.Msg.MOVE_TO_MARKED_SPOT, - 'DELETE': Blockly.Msg.DELETE - }; - }], - deleteBlock: function(block) { - // If this is the top block, we should shift focus to the previous tree - var topBlocks = blocklyApp.workspace.topBlocks_; - for (var i = 0; i < topBlocks.length; i++) { - if (topBlocks[i].id == block.id) { - this.treeService.goToPreviousTree(this.parentId); - break; - } - } - // If this is not the top block, we should change the active descendant of the tree. - - block.dispose(true); - }, - getMarkedBlockCompatibilityHTMLText: function(isCompatible) { - return this.utilsService.getMarkedBlockCompatibilityHTMLText(isCompatible); - }, - generateAriaLabelledByAttr: function() { - return this.utilsService.generateAriaLabelledByAttr.apply(this, - arguments); - }, - ngOnInit: function() { - var elementsNeedingIds = ['blockSummary', 'listItem', 'label', - 'cutListItem', 'cutButton', 'copyListItem', 'copyButton', - 'pasteBelow', 'pasteBelowButton', 'pasteAbove', 'pasteAboveButton', - 'markBelow', 'markBelowButton', 'markAbove', 'markAboveButton', - 'sendToSelectedListItem', 'sendToSelectedButton', 'delete', - 'deleteButton']; - for (var i = 0; i < this.block.inputList.length; i++) { - var inputBlock = this.block.inputList[i]; - if (inputBlock.connection && !inputBlock.connection.targetBlock()) { - elementsNeedingIds = elementsNeedingIds.concat( - ['inputList' + i, 'inputMenuLabel' + i, 'markSpot' + i, - 'markSpotButton' + i, 'paste' + i, 'pasteButton' + i]); - } - } - this.idMap = this.utilsService.generateIds(elementsNeedingIds); - this.idMap['parentList'] = this.generateParentListId(); - }, - generateParentListId: function() { - if (this.isTopBlock) { - return this.parentId + '-node0' - } else { - return this.utilsService.generateUniqueId(); - } - }, - getNoPreviousConnectionHTMLText: function(block) { - if (!block.previousConnection) { - return 'blockly-disabled'; - } else { - return ''; - } - }, - getNoNextConnectionHTMLText: function(block) { - if (!block.nextConnection) { - return 'blockly-disabled'; - } else { - return ''; - } - }, - checkParentList: function(parentList) { - blocklyApp.debug && console.log('setting parent list'); - var tree = parentList; - var regex = /^blockly-workspace-tree\d+$/; - while (tree && !tree.id.match(regex)) { - tree = tree.parentNode; - } - if (tree && tree.getAttribute('aria-activedescendant') == parentList.id) { - this.treeService.updateSelectedNode(parentList, tree, false); - } - }, - setId: function(block) { - if (this.isTopBlock) { - return this.parentId + '-node0'; - } - return this.treeService.createId(block); - }, - sendToSelected: function(block) { - if (this.clipboardService) { - this.clipboardService.pasteToMarkedConnection(block, false); - block.dispose(true); - alert('Block moved to marked spot: ' + block.toString()); - } - } - }); diff --git a/accessible/workspaceview.component.js b/accessible/workspaceview.component.js deleted file mode 100644 index ae0fccfbe..000000000 --- a/accessible/workspaceview.component.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * 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 that details how a Blockly.Workspace is - * rendered in AccessibleBlockly. - * @author madeeha@google.com (Madeeha Ghori) - */ - -blocklyApp.WorkspaceView = ng.core - .Component({ - selector: 'workspace-view', - viewInjector: [blocklyApp.ClipboardService], - template: ` - -
    - - - - -
    -
    -
      - -
    -
    - `, - directives: [blocklyApp.WorkspaceTreeView], - providers: [blocklyApp.TreeService] - }) - .Class({ - constructor: [blocklyApp.TreeService, function(_treeService) { - if (blocklyApp.workspace) { - this.workspace = blocklyApp.workspace; - this.treeService = _treeService; - } - // ACCESSIBLE_GLOBALS is a global variable defined by the containing - // page. It should contain a key, toolbarButtonConfig, whose - // corresponding value is an Array with two keys: 'text' and 'action'. - // The first is the text to display on the button, and the second is the - // function that gets run when the button is clicked. - this.toolbarButtonConfig = - ACCESSIBLE_GLOBALS && ACCESSIBLE_GLOBALS.toolbarButtonConfig ? - ACCESSIBLE_GLOBALS.toolbarButtonConfig : []; - this.stringMap = { - 'WORKSPACE': Blockly.Msg.WORKSPACE, - 'CLEAR_WORKSPACE': Blockly.Msg.CLEAR_WORKSPACE - }; - }], - onWorkspaceToolbarKeypress: function(event, id) { - this.treeService.onWorkspaceToolbarKeypress(event, id); - }, - onKeypress: function(event, tree){ - this.treeService.onKeypress(event, tree); - }, - getActiveElementId: function() { - return document.activeElement.id; - }, - makeId: function(index) { - return 'blockly-workspace-tree' + index; - }, - disableClearWorkspace: function() { - if (blocklyApp.workspace.topBlocks_.length){ - return undefined; - } else { - return 'blockly-disabled'; - } - } - }); diff --git a/appengine/README.txt b/appengine/README.txt index c836439c4..6ba262bba 100644 --- a/appengine/README.txt +++ b/appengine/README.txt @@ -35,7 +35,7 @@ blockly/ `- python_compressed.js Instructions for fetching the optional Closure library may be found here: - https://developers.google.com/blockly/hacking/closure + https://developers.google.com/blockly/guides/modify/web/closure Go to https://appengine.google.com/ and create your App Engine application. Modify the 'application' name of app.yaml to your App Engine application name. diff --git a/blockly_compressed.js b/blockly_compressed.js index 6edbf1df2..fd3d69f04 100644 --- a/blockly_compressed.js +++ b/blockly_compressed.js @@ -62,8 +62,8 @@ goog.string.escapeChar=function(a){if(a in goog.string.jsEscapeCache_)return goo goog.string.caseInsensitiveContains=function(a,b){return goog.string.contains(a.toLowerCase(),b.toLowerCase())};goog.string.countOf=function(a,b){return a&&b?a.split(b).length-1:0};goog.string.removeAt=function(a,b,c){var d=a;0<=b&&bb?1:0};goog.string.hashCode=function(a){for(var b=0,c=0;c>>0;return b};goog.string.uniqueStringCounter_=2147483648*Math.random()|0;goog.string.createUniqueString=function(){return"goog_"+goog.string.uniqueStringCounter_++}; +goog.string.compareVersions=function(a,b){for(var c=0,d=goog.string.trim(String(a)).split("."),e=goog.string.trim(String(b)).split("."),f=Math.max(d.length,e.length),g=0;0==c&&gb?1:0};goog.string.hashCode=function(a){for(var b=0,c=0;c>>0;return b};goog.string.uniqueStringCounter_=2147483648*Math.random()|0;goog.string.createUniqueString=function(){return"goog_"+goog.string.uniqueStringCounter_++}; goog.string.toNumber=function(a){var b=Number(a);return 0==b&&goog.string.isEmptyOrWhitespace(a)?NaN:b};goog.string.isLowerCamelCase=function(a){return/^[a-z]+([A-Z][a-z]*)*$/.test(a)};goog.string.isUpperCamelCase=function(a){return/^([A-Z][a-z]*)+$/.test(a)};goog.string.toCamelCase=function(a){return String(a).replace(/\-([a-z])/g,function(a,c){return c.toUpperCase()})};goog.string.toSelectorCase=function(a){return String(a).replace(/([A-Z])/g,"-$1").toLowerCase()}; goog.string.toTitleCase=function(a,b){var c=goog.isString(b)?goog.string.regExpEscape(b):"\\s";return a.replace(new RegExp("(^"+(c?"|["+c+"]+":"")+")([a-z])","g"),function(a,b,c){return b+c.toUpperCase()})};goog.string.capitalize=function(a){return String(a.charAt(0)).toUpperCase()+String(a.substr(1)).toLowerCase()};goog.string.parseInt=function(a){isFinite(a)&&(a=String(a));return goog.isString(a)?/^\s*-?0x/i.test(a)?parseInt(a,16):parseInt(a,10):NaN}; goog.string.splitLimit=function(a,b,c){a=a.split(b);for(var d=[];0c&&(c=e)}return-1==c?a:a.slice(c+1)}; @@ -91,7 +91,7 @@ goog.array.removeLast=function(a,b){var c=goog.array.lastIndexOf(a,b);return 0<= goog.array.concat=function(a){return Array.prototype.concat.apply(Array.prototype,arguments)};goog.array.join=function(a){return Array.prototype.concat.apply(Array.prototype,arguments)};goog.array.toArray=function(a){var b=a.length;if(0=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};goog.array.removeDuplicates=function(a,b,c){b=b||a;var d=function(a){return goog.isObject(a)?"o"+goog.getUid(a):(typeof a).charAt(0)+a};c=c||d;for(var d={},e=0,f=0;f>1,n;n=c?b.call(e,a[k],k,a):b(d,a[k]);0>1,m;m=c?b.call(e,a[k],k,a):b(d,a[k]);0b?1:ac*b?c+b:c};goog.math.lerp=function(a,b,c){return a+c*(b-a)};goog.math.nearlyEquals=function(a,b,c){return Math.abs(a-b)<=(c||1E-6)};goog.math.standardAngle=function(a){return goog.math.modulo(a,360)}; goog.math.standardAngleInRadians=function(a){return goog.math.modulo(a,2*Math.PI)};goog.math.toRadians=function(a){return a*Math.PI/180};goog.math.toDegrees=function(a){return 180*a/Math.PI};goog.math.angleDx=function(a,b){return b*Math.cos(goog.math.toRadians(a))};goog.math.angleDy=function(a,b){return b*Math.sin(goog.math.toRadians(a))};goog.math.angle=function(a,b,c,d){return goog.math.standardAngle(goog.math.toDegrees(Math.atan2(d-b,c-a)))}; goog.math.angleDifference=function(a,b){var c=goog.math.standardAngle(b)-goog.math.standardAngle(a);180=c&&(c=360+c);return c};goog.math.sign=Math.sign||function(a){return 0a?-1:a}; -goog.math.longestCommonSubsequence=function(a,b,c,d){c=c||function(a,b){return a==b};d=d||function(b,c){return a[b]};for(var e=a.length,f=b.length,g=[],h=0;hg[h][k-1]?h--:k--;return n}; +goog.math.longestCommonSubsequence=function(a,b,c,d){c=c||function(a,b){return a==b};d=d||function(b,c){return a[b]};for(var e=a.length,f=b.length,g=[],h=0;hg[h][k-1]?h--:k--;return m}; goog.math.sum=function(a){return goog.array.reduce(arguments,function(a,c){return a+c},0)};goog.math.average=function(a){return goog.math.sum.apply(null,arguments)/arguments.length};goog.math.sampleVariance=function(a){var b=arguments.length;if(2>b)return 0;var c=goog.math.average.apply(null,arguments);return goog.math.sum.apply(null,goog.array.map(arguments,function(a){return Math.pow(a-c,2)}))/(b-1)};goog.math.standardDeviation=function(a){return Math.sqrt(goog.math.sampleVariance.apply(null,arguments))}; goog.math.isInt=function(a){return isFinite(a)&&0==a%1};goog.math.isFiniteNumber=function(a){return isFinite(a)&&!isNaN(a)};goog.math.isNegativeZero=function(a){return 0==a&&0>1/a};goog.math.log10Floor=function(a){if(0a?1:0)}return 0==a?-Infinity:NaN};goog.math.safeFloor=function(a,b){goog.asserts.assert(!goog.isDef(b)||0");c=c.join("")}c=a.createElement(c);d&&(goog.isString(d)?c.className=d:goog.isArray(d)?c.className=d.join(" "):goog.dom.setProperties(c,d));2=d.keys_.length)throw goog.iter.StopIteration;var e=d.keys_[b++];return a?e:d.map_[e]};return e};goog.structs.Map.hasKey_=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)};goog.structs.Set=function(a){this.map_=new goog.structs.Map;a&&this.addAll(a)};goog.structs.Set.getKey_=function(a){var b=typeof a;return"object"==b&&a||"function"==b?"o"+goog.getUid(a):b.substr(0,1)+a};goog.structs.Set.prototype.getCount=function(){return this.map_.getCount()};goog.structs.Set.prototype.add=function(a){this.map_.set(goog.structs.Set.getKey_(a),a)};goog.structs.Set.prototype.addAll=function(a){a=goog.structs.getValues(a);for(var b=a.length,c=0;cb)return!1;!(a instanceof goog.structs.Set)&&5b)return!1;!(a instanceof goog.structs.Set)&&5 [end]\n\nJS stack traversal:\n"+goog.debug.getStacktrace(b)+ "-> "))}catch(e){return goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces("Exception trying to expose exception! You win, we lose. "+e)}}; goog.debug.createViewSourceUrl_=function(a){goog.isDefAndNotNull(a)||(a="");if(!/^https?:\/\//i.test(a))return goog.html.SafeUrl.fromConstant(goog.string.Const.from("sanitizedviewsrc"));a=goog.html.SafeUrl.sanitize(a);return goog.html.uncheckedconversions.safeUrlFromStringKnownToSatisfyTypeContract(goog.string.Const.from("view-source scheme plus HTTP/HTTPS URL"),"view-source:"+goog.html.SafeUrl.unwrap(a))}; @@ -885,8 +885,8 @@ this.width_+Blockly.BlockSvg.SEP_SPACE_X+Blockly.Scrollbar.scrollbarThickness&&( Blockly.Bubble.prototype.positionBubble_=function(){var a=this.anchorXY_.x,a=this.workspace_.RTL?a-(this.relativeLeft_+this.width_):a+this.relativeLeft_;this.bubbleGroup_.setAttribute("transform","translate("+a+","+(this.relativeTop_+this.anchorXY_.y)+")")};Blockly.Bubble.prototype.getBubbleSize=function(){return{width:this.width_,height:this.height_}}; Blockly.Bubble.prototype.setBubbleSize=function(a,b){var c=2*Blockly.Bubble.BORDER_WIDTH;a=Math.max(a,c+45);b=Math.max(b,c+20);this.width_=a;this.height_=b;this.bubbleBack_.setAttribute("width",a);this.bubbleBack_.setAttribute("height",b);this.resizeGroup_&&(this.workspace_.RTL?this.resizeGroup_.setAttribute("transform","translate("+2*Blockly.Bubble.BORDER_WIDTH+","+(b-c)+") scale(-1 1)"):this.resizeGroup_.setAttribute("transform","translate("+(a-c)+","+(b-c)+")"));this.rendered_&&(this.autoLayout_&& this.layoutBubble_(),this.positionBubble_(),this.renderArrow_());this.resizeCallback_&&this.resizeCallback_()}; -Blockly.Bubble.prototype.renderArrow_=function(){var a=[],b=this.width_/2,c=this.height_/2,d=-this.relativeLeft_,e=-this.relativeTop_;if(b==d&&c==e)a.push("M "+b+","+c);else{e-=c;d-=b;this.workspace_.RTL&&(d*=-1);var f=Math.sqrt(e*e+d*d),g=Math.acos(d/f);0>e&&(g=2*Math.PI-g);var h=g+Math.PI/2;h>2*Math.PI&&(h-=2*Math.PI);var k=Math.sin(h),n=Math.cos(h),p=this.getBubbleSize(),h=(p.width+p.height)/Blockly.Bubble.ARROW_THICKNESS,h=Math.min(h,p.width,p.height)/2,p=1-Blockly.Bubble.ANCHOR_RADIUS/f,d=b+ -p*d,e=c+p*e,p=b+h*n,l=c+h*k,b=b-h*n,c=c-h*k,k=g+this.arrow_radians_;k>2*Math.PI&&(k-=2*Math.PI);g=Math.sin(k)*f/Blockly.Bubble.ARROW_BEND;f=Math.cos(k)*f/Blockly.Bubble.ARROW_BEND;a.push("M"+p+","+l);a.push("C"+(p+f)+","+(l+g)+" "+d+","+e+" "+d+","+e);a.push("C"+d+","+e+" "+(b+f)+","+(c+g)+" "+b+","+c)}a.push("z");this.bubbleArrow_.setAttribute("d",a.join(" "))};Blockly.Bubble.prototype.setColour=function(a){this.bubbleBack_.setAttribute("fill",a);this.bubbleArrow_.setAttribute("fill",a)}; +Blockly.Bubble.prototype.renderArrow_=function(){var a=[],b=this.width_/2,c=this.height_/2,d=-this.relativeLeft_,e=-this.relativeTop_;if(b==d&&c==e)a.push("M "+b+","+c);else{e-=c;d-=b;this.workspace_.RTL&&(d*=-1);var f=Math.sqrt(e*e+d*d),g=Math.acos(d/f);0>e&&(g=2*Math.PI-g);var h=g+Math.PI/2;h>2*Math.PI&&(h-=2*Math.PI);var k=Math.sin(h),m=Math.cos(h),p=this.getBubbleSize(),h=(p.width+p.height)/Blockly.Bubble.ARROW_THICKNESS,h=Math.min(h,p.width,p.height)/2,p=1-Blockly.Bubble.ANCHOR_RADIUS/f,d=b+ +p*d,e=c+p*e,p=b+h*m,l=c+h*k,b=b-h*m,c=c-h*k,k=g+this.arrow_radians_;k>2*Math.PI&&(k-=2*Math.PI);g=Math.sin(k)*f/Blockly.Bubble.ARROW_BEND;f=Math.cos(k)*f/Blockly.Bubble.ARROW_BEND;a.push("M"+p+","+l);a.push("C"+(p+f)+","+(l+g)+" "+d+","+e+" "+d+","+e);a.push("C"+d+","+e+" "+(b+f)+","+(c+g)+" "+b+","+c)}a.push("z");this.bubbleArrow_.setAttribute("d",a.join(" "))};Blockly.Bubble.prototype.setColour=function(a){this.bubbleBack_.setAttribute("fill",a);this.bubbleArrow_.setAttribute("fill",a)}; Blockly.Bubble.prototype.dispose=function(){Blockly.Bubble.unbindDragEvents_();goog.dom.removeNode(this.bubbleGroup_);this.shape_=this.content_=this.workspace_=this.resizeGroup_=this.bubbleBack_=this.bubbleArrow_=this.bubbleGroup_=null};Blockly.Icon=function(a){this.block_=a};Blockly.Icon.prototype.collapseHidden=!0;Blockly.Icon.prototype.SIZE=17;Blockly.Icon.prototype.bubble_=null;Blockly.Icon.prototype.iconXY_=null;Blockly.Icon.prototype.createIcon=function(){this.iconGroup_||(this.iconGroup_=Blockly.createSvgElement("g",{"class":"blocklyIconGroup"},null),this.drawIcon_(this.iconGroup_),this.block_.getSvgRoot().appendChild(this.iconGroup_),Blockly.bindEvent_(this.iconGroup_,"mouseup",this,this.iconClick_),this.updateEditable())}; Blockly.Icon.prototype.dispose=function(){goog.dom.removeNode(this.iconGroup_);this.iconGroup_=null;this.setVisible(!1);this.block_=null};Blockly.Icon.prototype.updateEditable=function(){this.block_.isInFlyout||!this.block_.isEditable()?Blockly.addClass_(this.iconGroup_,"blocklyIconGroupReadonly"):Blockly.removeClass_(this.iconGroup_,"blocklyIconGroupReadonly")};Blockly.Icon.prototype.isVisible=function(){return!!this.bubble_}; Blockly.Icon.prototype.iconClick_=function(a){Blockly.dragMode_!=Blockly.DRAG_FREE&&(this.block_.isInFlyout||Blockly.isRightButton(a)||this.setVisible(!this.isVisible()))};Blockly.Icon.prototype.updateColour=function(){this.isVisible()&&this.bubble_.setColour(this.block_.getColour())}; @@ -904,22 +904,22 @@ Blockly.Comment.prototype.setBubbleSize=function(a,b){this.textarea_?this.bubble Blockly.Comment.prototype.dispose=function(){Blockly.Events.isEnabled()&&this.setText("");this.block_.comment=null;Blockly.Icon.prototype.dispose.call(this)};Blockly.Connection=function(a,b){this.sourceBlock_=a;this.type=b;a.workspace.connectionDBList&&(this.db_=a.workspace.connectionDBList[b],this.dbOpposite_=a.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[b]],this.hidden_=!this.db_)};Blockly.Connection.CAN_CONNECT=0;Blockly.Connection.REASON_SELF_CONNECTION=1;Blockly.Connection.REASON_WRONG_TYPE=2;Blockly.Connection.REASON_TARGET_NULL=3;Blockly.Connection.REASON_CHECKS_FAILED=4;Blockly.Connection.REASON_DIFFERENT_WORKSPACES=5; Blockly.Connection.REASON_SHADOW_PARENT=6;Blockly.Connection.prototype.targetConnection=null;Blockly.Connection.prototype.check_=null;Blockly.Connection.prototype.shadowDom_=null;Blockly.Connection.prototype.x_=0;Blockly.Connection.prototype.y_=0;Blockly.Connection.prototype.inDB_=!1;Blockly.Connection.prototype.db_=null;Blockly.Connection.prototype.dbOpposite_=null;Blockly.Connection.prototype.hidden_=null; Blockly.Connection.prototype.connect_=function(a){var b=this,c=b.getSourceBlock(),d=a.getSourceBlock();a.isConnected()&&a.disconnect();if(b.isConnected()){var e=b.targetBlock(),f=b.getShadowDom();b.setShadowDom(null);if(e.isShadow())f=Blockly.Xml.blockToDom(e),e.dispose(),e=null;else if(b.type==Blockly.INPUT_VALUE){if(!e.outputConnection)throw"Orphan block does not have an output connection.";var g=Blockly.Connection.lastConnectionInRow_(d,e);g&&(e.outputConnection.connect(g),e=null)}else if(b.type== -Blockly.NEXT_STATEMENT){if(!e.previousConnection)throw"Orphan block does not have a previous connection.";for(g=d;g.nextConnection;)if(g.nextConnection.isConnected())g=g.getNextBlock();else{e.previousConnection.checkType_(g.nextConnection)&&(g.nextConnection.connect(e.previousConnection),e=null);break}}if(e&&(b.disconnect(),Blockly.Events.recordUndo)){var h=Blockly.Events.getGroup();setTimeout(function(){e.workspace&&!e.getParent()&&(Blockly.Events.setGroup(h),e.outputConnection?e.outputConnection.bumpAwayFrom_(b): -e.previousConnection&&e.previousConnection.bumpAwayFrom_(b),Blockly.Events.setGroup(!1))},Blockly.BUMP_DELAY)}b.setShadowDom(f)}var k;Blockly.Events.isEnabled()&&(k=new Blockly.Events.Move(d));Blockly.Connection.connectReciprocally_(b,a);d.setParent(c);k&&(k.recordNew(),Blockly.Events.fire(k))}; +Blockly.NEXT_STATEMENT){if(!e.previousConnection)throw"Orphan block does not have a previous connection.";for(g=d;g.nextConnection;){var h=g.getNextBlock();if(h&&!h.isShadow())g=h;else{e.previousConnection.checkType_(g.nextConnection)&&(g.nextConnection.connect(e.previousConnection),e=null);break}}}if(e&&(b.disconnect(),Blockly.Events.recordUndo)){var k=Blockly.Events.getGroup();setTimeout(function(){e.workspace&&!e.getParent()&&(Blockly.Events.setGroup(k),e.outputConnection?e.outputConnection.bumpAwayFrom_(b): +e.previousConnection&&e.previousConnection.bumpAwayFrom_(b),Blockly.Events.setGroup(!1))},Blockly.BUMP_DELAY)}b.setShadowDom(f)}var m;Blockly.Events.isEnabled()&&(m=new Blockly.Events.Move(d));Blockly.Connection.connectReciprocally_(b,a);d.setParent(c);m&&(m.recordNew(),Blockly.Events.fire(m))}; Blockly.Connection.prototype.dispose=function(){if(this.isConnected())throw"Disconnect connection before disposing of it.";this.inDB_&&this.db_.removeConnection_(this);Blockly.highlightedConnection_==this&&(Blockly.highlightedConnection_=null);Blockly.localConnection_==this&&(Blockly.localConnection_=null);this.dbOpposite_=this.db_=null};Blockly.Connection.prototype.getSourceBlock=function(){return this.sourceBlock_}; Blockly.Connection.prototype.isSuperior=function(){return this.type==Blockly.INPUT_VALUE||this.type==Blockly.NEXT_STATEMENT};Blockly.Connection.prototype.isConnected=function(){return!!this.targetConnection}; Blockly.Connection.prototype.canConnectWithReason_=function(a){if(!a)return Blockly.Connection.REASON_TARGET_NULL;if(this.isSuperior())var b=this.sourceBlock_,c=a.getSourceBlock();else c=this.sourceBlock_,b=a.getSourceBlock();return b&&b==c?Blockly.Connection.REASON_SELF_CONNECTION:a.type!=Blockly.OPPOSITE_TYPE[this.type]?Blockly.Connection.REASON_WRONG_TYPE:b&&c&&b.workspace!==c.workspace?Blockly.Connection.REASON_DIFFERENT_WORKSPACES:this.checkType_(a)?b.isShadow()&&!c.isShadow()?Blockly.Connection.REASON_SHADOW_PARENT: Blockly.Connection.CAN_CONNECT:Blockly.Connection.REASON_CHECKS_FAILED}; Blockly.Connection.prototype.checkConnection_=function(a){switch(this.canConnectWithReason_(a)){case Blockly.Connection.CAN_CONNECT:break;case Blockly.Connection.REASON_SELF_CONNECTION:throw"Attempted to connect a block to itself.";case Blockly.Connection.REASON_DIFFERENT_WORKSPACES:throw"Blocks not on same workspace.";case Blockly.Connection.REASON_WRONG_TYPE:throw"Attempt to connect incompatible types.";case Blockly.Connection.REASON_TARGET_NULL:throw"Target connection is null.";case Blockly.Connection.REASON_CHECKS_FAILED:throw"Connection checks failed."; case Blockly.Connection.REASON_SHADOW_PARENT:throw"Connecting non-shadow to shadow block.";default:throw"Unknown connection failure: this should never happen!";}}; -Blockly.Connection.prototype.isConnectionAllowed=function(a){var b=this.canConnectWithReason_(a);if(b!=Blockly.Connection.CAN_CONNECT&&b!=Blockly.Connection.REASON_MUST_DISCONNECT)return!1;if(a.type==Blockly.OUTPUT_VALUE||a.type==Blockly.PREVIOUS_STATEMENT)if(a.isConnected()||this.isConnected())return!1;return a.type==Blockly.INPUT_VALUE&&a.isConnected()&&!a.targetBlock().isMovable()&&!a.targetBlock().isShadow()||this.type==Blockly.PREVIOUS_STATEMENT&&a.isConnected()&&!this.sourceBlock_.nextConnection|| --1!=Blockly.draggingConnections_.indexOf(a)?!1:!0};Blockly.Connection.prototype.connect=function(a){this.targetConnection!=a&&(this.checkConnection_(a),this.isSuperior()?this.connect_(a):a.connect_(this))};Blockly.Connection.connectReciprocally_=function(a,b){goog.asserts.assert(a&&b,"Cannot connect null connections.");a.targetConnection=b;b.targetConnection=a}; +Blockly.Connection.prototype.isConnectionAllowed=function(a){if(this.canConnectWithReason_(a)!=Blockly.Connection.CAN_CONNECT)return!1;if(a.type==Blockly.OUTPUT_VALUE||a.type==Blockly.PREVIOUS_STATEMENT)if(a.isConnected()||this.isConnected())return!1;return a.type==Blockly.INPUT_VALUE&&a.isConnected()&&!a.targetBlock().isMovable()&&!a.targetBlock().isShadow()||this.type==Blockly.PREVIOUS_STATEMENT&&a.isConnected()&&!this.sourceBlock_.nextConnection||-1!=Blockly.draggingConnections_.indexOf(a)?!1: +!0};Blockly.Connection.prototype.connect=function(a){this.targetConnection!=a&&(this.checkConnection_(a),this.isSuperior()?this.connect_(a):a.connect_(this))};Blockly.Connection.connectReciprocally_=function(a,b){goog.asserts.assert(a&&b,"Cannot connect null connections.");a.targetConnection=b;b.targetConnection=a}; Blockly.Connection.singleConnection_=function(a,b){for(var c=!1,d=0;dBlockly.Tooltip.RADIUS_OK&&Blockly.Tooltip.hide()}else Blockly.Tooltip.poisonedElement_!=Blockly.Tooltip.element_&&(clearTimeout(Blockly.Tooltip.showPid_),Blockly.Tooltip.lastX_=a.pageX,Blockly.Tooltip.lastY_=a.pageY, Blockly.Tooltip.showPid_=setTimeout(Blockly.Tooltip.show_,Blockly.Tooltip.HOVER_MS))};Blockly.Tooltip.hide=function(){Blockly.Tooltip.visible&&(Blockly.Tooltip.visible=!1,Blockly.Tooltip.DIV&&(Blockly.Tooltip.DIV.style.display="none"));clearTimeout(Blockly.Tooltip.showPid_)}; -Blockly.Tooltip.show_=function(){Blockly.Tooltip.poisonedElement_=Blockly.Tooltip.element_;if(Blockly.Tooltip.DIV){goog.dom.removeChildren(Blockly.Tooltip.DIV);for(var a=Blockly.Tooltip.element_.tooltip;goog.isFunction(a);)a=a();for(var a=Blockly.Tooltip.wrap_(a,Blockly.Tooltip.LIMIT),a=a.split("\n"),b=0;bb.height+window.scrollY&&(d-=Blockly.Tooltip.DIV.offsetHeight+2*Blockly.Tooltip.OFFSET_Y);a?c=Math.max(Blockly.Tooltip.MARGINS-window.scrollX,c):c+Blockly.Tooltip.DIV.offsetWidth>b.width+window.scrollX-2*Blockly.Tooltip.MARGINS&& -(c=b.width-Blockly.Tooltip.DIV.offsetWidth-2*Blockly.Tooltip.MARGINS);Blockly.Tooltip.DIV.style.top=d+"px";Blockly.Tooltip.DIV.style.left=c+"px"}}; -Blockly.Tooltip.wrap_=function(a,b){if(a.length<=b)return a;for(var c=a.trim().split(/\s+/),d=0;db&&(b=c[d].length);var e,d=-Infinity,f,g=1;do{e=d;f=a;for(var h=[],k=c.length/g,n=1,d=0;de);return f}; -Blockly.Tooltip.wrapScore_=function(a,b,c){for(var d=[0],e=[],f=0;fd&&(d=h,e=g)}return e?Blockly.Tooltip.wrapMutate_(a,e,c):b};Blockly.Tooltip.wrapToText_=function(a,b){for(var c=[],d=0;d=this.length)return-1;for(var c=a.y_,d=b;0<=d&&this[d].y_==c;){if(this[d]==a)return d;d--}for(;ba.y_)c=d;else{b=d;break}}return b};Blockly.ConnectionDB.prototype.removeConnection_=function(a){if(!a.inDB_)throw"Connection not in database.";var b=this.findConnection(a);if(-1==b)throw"Unable to find connection in connectionDB.";a.inDB_=!1;this.splice(b,1)}; -Blockly.ConnectionDB.prototype.getNeighbours=function(a,b){function c(a){var c=e-d[a].x_,g=f-d[a].y_;Math.sqrt(c*c+g*g)<=b&&n.push(d[a]);return g"!=d.slice(-2)&&(b+=" ")}a=a.join("\n");a=a.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g,"$1");return a.replace(/^\n/,"")}; Blockly.Xml.textToDom=function(a){(a=(new DOMParser).parseFromString(a,"text/xml"))&&a.firstChild&&"xml"==a.firstChild.nodeName.toLowerCase()&&a.firstChild===a.lastChild||goog.asserts.fail("Blockly.Xml.textToDom did not obtain a valid XML tree.");return a.firstChild}; -Blockly.Xml.domToWorkspace=function(a,b){if(a instanceof Blockly.Workspace){var c=a;a=b;b=c;console.warn("Deprecated call to Blockly.Xml.domToWorkspace, swap the arguments.")}var d;b.RTL&&(d=b.getWidth());Blockly.Field.startCache();var c=a.childNodes.length,e=Blockly.Events.getGroup();e||Blockly.Events.setGroup(!0);for(var f=0;fb.bottomRight.x&&(b.bottomRight.x=d.bottomRight.x);d.topLeft.yb.bottomRight.y&&(b.bottomRight.y=d.bottomRight.y)}return{x:b.topLeft.x,y:b.topLeft.y,width:b.bottomRight.x- b.topLeft.x,height:b.bottomRight.y-b.topLeft.y}};Blockly.WorkspaceSvg.prototype.cleanUp_=function(){Blockly.Events.setGroup(!0);for(var a=this.getTopBlocks(!0),b=0,c=0,d;d=a[c];c++){var e=d.getRelativeToSurfaceXY();d.moveBy(-e.x,b-e.y);d.snapToGrid();b=d.getRelativeToSurfaceXY().y+d.getHeightWidth().height+Blockly.BlockSvg.MIN_BLOCK_Y}Blockly.Events.setGroup(!1);Blockly.resizeSvgContents(this)}; Blockly.WorkspaceSvg.prototype.showContextMenu_=function(a){function b(a){if(a.isDeletable())l=l.concat(a.getDescendants());else{a=a.getChildren();for(var c=0;cl.length||window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace("%1",String(l.length))))&&c()}};d.push(g);Blockly.ContextMenu.show(a,d,this.RTL)}}; Blockly.WorkspaceSvg.prototype.loadAudio_=function(a,b){if(a.length){try{var c=new window.Audio}catch(h){return}for(var d,e=0;eb?!1:Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this,a)}; Blockly.RenderedConnection.prototype.disconnectInternal_=function(a,b){Blockly.RenderedConnection.superClass_.disconnectInternal_.call(this,a,b);a.rendered&&a.render();b.rendered&&(b.updateDisabled(),b.render())}; -Blockly.RenderedConnection.prototype.respawnShadow_=function(){var a=this.getSourceBlock(),b=this.getShadowDom();if(a.workspace&&b&&Blockly.Events.recordUndo){b=Blockly.RenderedConnection.superClass_.respawnShadow_.call(this);if(!b)throw"Couldn't respawn the shadow block that should exist here.";b.initSvg();b.render(!1);a.rendered&&a.render()}};Blockly.RenderedConnection.prototype.neighbours_=function(a){return this.dbOpposite_.getNeighbours(this,a)}; +Blockly.RenderedConnection.prototype.respawnShadow_=function(){var a=this.getSourceBlock(),b=this.getShadowDom();if(a.workspace&&b&&Blockly.Events.recordUndo){Blockly.RenderedConnection.superClass_.respawnShadow_.call(this);b=this.targetBlock();if(!b)throw"Couldn't respawn the shadow block that should exist here.";b.initSvg();b.render(!1);a.rendered&&a.render()}};Blockly.RenderedConnection.prototype.neighbours_=function(a){return this.dbOpposite_.getNeighbours(this,a)}; Blockly.RenderedConnection.prototype.connect_=function(a){Blockly.RenderedConnection.superClass_.connect_.call(this,a);var b=this.getSourceBlock();a=a.getSourceBlock();b.rendered&&b.updateDisabled();a.rendered&&a.updateDisabled();b.rendered&&a.rendered&&(this.type==Blockly.NEXT_STATEMENT||this.type==Blockly.PREVIOUS_STATEMENT?a.render():b.render())};Blockly.BlockSvg=function(a,b,c){this.svgGroup_=Blockly.createSvgElement("g",{},null);this.svgPathDark_=Blockly.createSvgElement("path",{"class":"blocklyPathDark",transform:"translate(1,1)"},this.svgGroup_);this.svgPath_=Blockly.createSvgElement("path",{"class":"blocklyPath"},this.svgGroup_);this.svgPathLight_=Blockly.createSvgElement("path",{"class":"blocklyPathLight"},this.svgGroup_);this.svgPath_.tooltip=this;this.rendered=!1;Blockly.Tooltip.bindMouseEvents(this.svgPath_);Blockly.BlockSvg.superClass_.constructor.call(this, a,b,c)};goog.inherits(Blockly.BlockSvg,Blockly.Block);Blockly.BlockSvg.prototype.height=0;Blockly.BlockSvg.prototype.width=0;Blockly.BlockSvg.prototype.dragStartXY_=null;Blockly.BlockSvg.INLINE=-1; Blockly.BlockSvg.prototype.initSvg=function(){goog.asserts.assert(this.workspace.rendered,"Workspace is headless.");for(var a=0,b;b=this.inputList[a];a++)b.init();b=this.getIcons();for(a=0;aa.viewHeight+f||a.contentLeft<(b.RTL?a.viewLeft:e)||a.contentLeft+a.contentWidth>(b.RTL?a.viewWidth:a.viewWidth+e))for(var g=c.getTopBlocks(!1),h=0,k;k=g[h];h++){var n=k.getRelativeToSurfaceXY(),p=k.getHeightWidth(),l=f+25-p.height-n.y;0l&&k.moveBy(0,l);l=25+e-n.x-(b.RTL?0:p.width);0n&&k.moveBy(n,0)}}});Blockly.svgResize(c);Blockly.WidgetDiv.createDom();Blockly.Tooltip.createDom(); +f||a.contentTop+a.contentHeight>a.viewHeight+f||a.contentLeft<(b.RTL?a.viewLeft:e)||a.contentLeft+a.contentWidth>(b.RTL?a.viewWidth:a.viewWidth+e))for(var g=c.getTopBlocks(!1),h=0,k;k=g[h];h++){var m=k.getRelativeToSurfaceXY(),p=k.getHeightWidth(),l=f+25-p.height-m.y;0l&&k.moveBy(0,l);l=25+e-m.x-(b.RTL?0:p.width);0m&&k.moveBy(m,0)}}});Blockly.svgResize(c);Blockly.WidgetDiv.createDom();Blockly.Tooltip.createDom(); return c}; Blockly.init_=function(a){var b=a.options,c=a.getParentSvg();Blockly.bindEvent_(c,"contextmenu",null,function(a){Blockly.isTargetInput_(a)||a.preventDefault()});Blockly.bindEvent_(window,"resize",null,function(){Blockly.hideChaff(!0);Blockly.svgResize(a)});Blockly.inject.bindDocumentEvents_();b.languageTree&&(a.toolbox_?a.toolbox_.init(a):a.flyout_&&(a.flyout_.init(a),a.flyout_.show(b.languageTree.childNodes),a.flyout_.scrollToStart(),a.scrollX=a.flyout_.width_,b.toolboxPosition==Blockly.TOOLBOX_AT_RIGHT&&(a.scrollX*= -1),a.translate(a.scrollX,0)));b.hasScrollbars&&(a.scrollbar=new Blockly.ScrollbarPair(a),a.scrollbar.resize());b.hasSounds&&Blockly.inject.loadSounds_(b.pathToMedia,a)}; @@ -1402,7 +1399,10 @@ Blockly.createSvgElement=function(a,b,c,d){a=document.createElementNS(Blockly.SV Blockly.shortestStringLength=function(a){if(!a.length)return 0;for(var b=a[0].length,c=1;c=g?(c=2,e=g,(g=d.join(""))&&b.push(g),d.length=0):(d.push("%",g),c=0):2==c&&("0"<=g&&"9">=g?e+=g:(b.push(parseInt(e,10)),f--,c=0))}(g=d.join(""))&&b.push(g);return b};Blockly.genUid=function(){for(var a=Blockly.genUid.soup_.length,b=[],c=0;20>c;c++)b[c]=Blockly.genUid.soup_.charAt(Math.random()*a);return b.join("")}; -Blockly.genUid.soup_="!#%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";var CLOSURE_DEFINES={"goog.DEBUG":!1};Blockly.mainWorkspace=null;Blockly.selected=null;Blockly.highlightedConnection_=null;Blockly.localConnection_=null;Blockly.draggingConnections_=[];Blockly.clipboardXml_=null;Blockly.clipboardSource_=null;Blockly.dragMode_=Blockly.DRAG_NONE;Blockly.onTouchUpWrapper_=null;Blockly.hueToRgb=function(a){return goog.color.hsvToHex(a,Blockly.HSV_SATURATION,255*Blockly.HSV_VALUE)};Blockly.svgSize=function(a){return{width:a.cachedWidth_,height:a.cachedHeight_}}; +Blockly.genUid.soup_="!#%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";Blockly.utils.wrap=function(a,b){for(var c=a.split("\n"),d=0;db&&(b=c[d].length);var e,d=-Infinity,f,g=1;do{e=d;f=a;for(var h=[],k=c.length/g,m=1,d=0;de);return f}; +Blockly.utils.wrapScore_=function(a,b,c){for(var d=[0],e=[],f=0;fd&&(d=h,e=g)}return e?Blockly.utils.wrapMutate_(a,e,c):b};Blockly.utils.wrapToText_=function(a,b){for(var c=[],d=0;dBlockly.DRAG_RADIUS&& diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 9868f539b..fb8afa9e7 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -34,7 +34,7 @@ window.BLOCKLY_BOOT = function() { // Execute after Closure has loaded. if (!window.goog) { alert('Error: Closure not found. Read this:\n' + - 'developers.google.com/blockly/hacking/closure'); + 'developers.google.com/blockly/guides/modify/web/closure'); } dir = window.BLOCKLY_DIR.match(/[^\/]+$/)[0]; } @@ -276,8 +276,8 @@ goog.addDependency("dom/controlrange.js", ['goog.dom.ControlRange', 'goog.dom.Co goog.addDependency("dom/controlrange_test.js", ['goog.dom.ControlRangeTest'], ['goog.dom', 'goog.dom.ControlRange', 'goog.dom.RangeType', 'goog.dom.TagName', 'goog.dom.TextRange', 'goog.testing.dom', 'goog.testing.jsunit', 'goog.userAgent']); goog.addDependency("dom/dataset.js", ['goog.dom.dataset'], ['goog.string', 'goog.userAgent.product']); goog.addDependency("dom/dataset_test.js", ['goog.dom.datasetTest'], ['goog.dom', 'goog.dom.dataset', 'goog.testing.jsunit']); -goog.addDependency("dom/dom.js", ['goog.dom', 'goog.dom.Appendable', 'goog.dom.DomHelper'], ['goog.array', 'goog.asserts', 'goog.dom.BrowserFeature', 'goog.dom.NodeType', 'goog.dom.TagName', 'goog.dom.safe', 'goog.html.SafeHtml', 'goog.math.Coordinate', 'goog.math.Size', 'goog.object', 'goog.string', 'goog.string.Unicode', 'goog.userAgent']); -goog.addDependency("dom/dom_test.js", ['goog.dom.dom_test'], ['goog.dom', 'goog.dom.BrowserFeature', 'goog.dom.DomHelper', 'goog.dom.InputType', 'goog.dom.NodeType', 'goog.dom.TagName', 'goog.functions', 'goog.html.testing', 'goog.object', 'goog.string.Unicode', 'goog.testing.PropertyReplacer', 'goog.testing.asserts', 'goog.userAgent', 'goog.userAgent.product', 'goog.userAgent.product.isVersion']); +goog.addDependency("dom/dom.js", ['goog.dom', 'goog.dom.Appendable', 'goog.dom.DomHelper'], ['goog.array', 'goog.asserts', 'goog.dom.BrowserFeature', 'goog.dom.NodeType', 'goog.dom.TagName', 'goog.dom.safe', 'goog.html.SafeHtml', 'goog.html.uncheckedconversions', 'goog.math.Coordinate', 'goog.math.Size', 'goog.object', 'goog.string', 'goog.string.Unicode', 'goog.userAgent']); +goog.addDependency("dom/dom_test.js", ['goog.dom.dom_test'], ['goog.array', 'goog.dom', 'goog.dom.BrowserFeature', 'goog.dom.DomHelper', 'goog.dom.InputType', 'goog.dom.NodeType', 'goog.dom.TagName', 'goog.functions', 'goog.html.testing', 'goog.object', 'goog.string.Const', 'goog.string.Unicode', 'goog.testing.PropertyReplacer', 'goog.testing.asserts', 'goog.userAgent', 'goog.userAgent.product', 'goog.userAgent.product.isVersion']); goog.addDependency("dom/fontsizemonitor.js", ['goog.dom.FontSizeMonitor', 'goog.dom.FontSizeMonitor.EventType'], ['goog.dom', 'goog.dom.TagName', 'goog.events', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.userAgent']); goog.addDependency("dom/fontsizemonitor_test.js", ['goog.dom.FontSizeMonitorTest'], ['goog.dom', 'goog.dom.FontSizeMonitor', 'goog.dom.TagName', 'goog.events', 'goog.events.Event', 'goog.testing.PropertyReplacer', 'goog.testing.events', 'goog.testing.jsunit', 'goog.userAgent']); goog.addDependency("dom/forms.js", ['goog.dom.forms'], ['goog.dom.InputType', 'goog.dom.TagName', 'goog.structs.Map', 'goog.window']); @@ -781,6 +781,7 @@ goog.addDependency("math/exponentialbackoff.js", ['goog.math.ExponentialBackoff' goog.addDependency("math/exponentialbackoff_test.js", ['goog.math.ExponentialBackoffTest'], ['goog.math.ExponentialBackoff', 'goog.testing.jsunit']); goog.addDependency("math/integer.js", ['goog.math.Integer'], []); goog.addDependency("math/integer_test.js", ['goog.math.IntegerTest'], ['goog.math.Integer', 'goog.testing.jsunit']); +goog.addDependency("math/irect.js", ['goog.math.IRect'], []); goog.addDependency("math/line.js", ['goog.math.Line'], ['goog.math', 'goog.math.Coordinate']); goog.addDependency("math/line_test.js", ['goog.math.LineTest'], ['goog.math.Coordinate', 'goog.math.Line', 'goog.testing.jsunit']); goog.addDependency("math/long.js", ['goog.math.Long'], ['goog.reflect']); @@ -797,7 +798,7 @@ goog.addDependency("math/range.js", ['goog.math.Range'], ['goog.asserts']); goog.addDependency("math/range_test.js", ['goog.math.RangeTest'], ['goog.math.Range', 'goog.testing.jsunit']); goog.addDependency("math/rangeset.js", ['goog.math.RangeSet'], ['goog.array', 'goog.iter.Iterator', 'goog.iter.StopIteration', 'goog.math.Range']); goog.addDependency("math/rangeset_test.js", ['goog.math.RangeSetTest'], ['goog.iter', 'goog.math.Range', 'goog.math.RangeSet', 'goog.testing.jsunit']); -goog.addDependency("math/rect.js", ['goog.math.Rect'], ['goog.asserts', 'goog.math.Box', 'goog.math.Coordinate', 'goog.math.Size']); +goog.addDependency("math/rect.js", ['goog.math.Rect'], ['goog.asserts', 'goog.math.Box', 'goog.math.Coordinate', 'goog.math.IRect', 'goog.math.Size']); goog.addDependency("math/rect_test.js", ['goog.math.RectTest'], ['goog.math.Box', 'goog.math.Coordinate', 'goog.math.Rect', 'goog.math.Size', 'goog.testing.jsunit']); goog.addDependency("math/size.js", ['goog.math.Size'], []); goog.addDependency("math/size_test.js", ['goog.math.SizeTest'], ['goog.math.Size', 'goog.testing.jsunit']); diff --git a/build.py b/build.py index 52701d8d9..211ca156e 100755 --- a/build.py +++ b/build.py @@ -113,7 +113,7 @@ window.BLOCKLY_BOOT = function() { // Execute after Closure has loaded. if (!window.goog) { alert('Error: Closure not found. Read this:\\n' + - 'developers.google.com/blockly/hacking/closure'); + 'developers.google.com/blockly/guides/modify/web/closure'); } dir = window.BLOCKLY_DIR.match(/[^\\/]+$/)[0]; } @@ -452,7 +452,7 @@ if __name__ == "__main__": "Please rename this directory.") else: print("""Error: Closure not found. Read this: -https://developers.google.com/blockly/hacking/closure""") +developers.google.com/blockly/guides/modify/web/closure""") sys.exit(1) search_paths = calcdeps.ExpandDirectories( diff --git a/core/block_svg.js b/core/block_svg.js index c84220606..8fc64f3a8 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -343,6 +343,7 @@ Blockly.BlockSvg.prototype.moveBy = function(dx, dy) { 'translate(' + (xy.x + dx) + ',' + (xy.y + dy) + ')'); this.moveConnections_(dx, dy); event.recordNew(); + Blockly.resizeSvgContents(this.workspace); Blockly.Events.fire(event); }; @@ -1036,7 +1037,7 @@ Blockly.BlockSvg.prototype.disposeUiEffect = function() { this.workspace.getParentSvg().appendChild(clone); clone.bBox_ = clone.getBBox(); // Start the animation. - Blockly.BlockSvg.disposeUiStep_(clone, this.RTL, new Date(), + Blockly.BlockSvg.disposeUiStep_(clone, this.RTL, new Date, this.workspace.scale); }; @@ -1051,7 +1052,7 @@ Blockly.BlockSvg.prototype.disposeUiEffect = function() { * @private */ Blockly.BlockSvg.disposeUiStep_ = function(clone, rtl, start, workspaceScale) { - var ms = (new Date()) - start; + var ms = new Date - start; var percent = ms / 150; if (percent > 1) { goog.dom.removeNode(clone); @@ -1093,7 +1094,7 @@ Blockly.BlockSvg.prototype.connectionUiEffect = function() { 'stroke': '#888', 'stroke-width': 10}, this.workspace.getParentSvg()); // Start the animation. - Blockly.BlockSvg.connectionUiStep_(ripple, new Date(), this.workspace.scale); + Blockly.BlockSvg.connectionUiStep_(ripple, new Date, this.workspace.scale); }; /** @@ -1104,7 +1105,7 @@ Blockly.BlockSvg.prototype.connectionUiEffect = function() { * @private */ Blockly.BlockSvg.connectionUiStep_ = function(ripple, start, workspaceScale) { - var ms = (new Date()) - start; + var ms = new Date - start; var percent = ms / 150; if (percent > 1) { goog.dom.removeNode(ripple); @@ -1135,7 +1136,7 @@ Blockly.BlockSvg.prototype.disconnectUiEffect = function() { magnitude *= -1; } // Start the animation. - Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date()); + Blockly.BlockSvg.disconnectUiStep_(this.svgGroup_, magnitude, new Date); }; /** @@ -1149,7 +1150,7 @@ Blockly.BlockSvg.disconnectUiStep_ = function(group, magnitude, start) { var DURATION = 200; // Milliseconds. var WIGGLES = 3; // Half oscillations. - var ms = (new Date()) - start; + var ms = new Date - start; var percent = ms / DURATION; if (percent > 1) { diff --git a/core/connection.js b/core/connection.js index d245c3548..65d7fd9cf 100644 --- a/core/connection.js +++ b/core/connection.js @@ -346,8 +346,7 @@ Blockly.Connection.prototype.checkConnection_ = function(target) { Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { // Type checking. var canConnect = this.canConnectWithReason_(candidate); - if (canConnect != Blockly.Connection.CAN_CONNECT && - canConnect != Blockly.Connection.REASON_MUST_DISCONNECT) { + if (canConnect != Blockly.Connection.CAN_CONNECT) { return false; } diff --git a/core/field_textinput.js b/core/field_textinput.js index 0781e1c04..994db7263 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -198,6 +198,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) { this.sourceBlock_.render(); } this.resizeEditor_(); + Blockly.svgResize(this.sourceBlock_.workspace); }; /** diff --git a/core/flyout.js b/core/flyout.js index 52027e428..66d5c2c6e 100644 --- a/core/flyout.js +++ b/core/flyout.js @@ -1040,7 +1040,9 @@ Blockly.Flyout.prototype.reflowHorizontal = function(blocks) { } // Record the height for .getMetrics_ and .position. this.height_ = flyoutHeight; - Blockly.resizeSvgContents(this.workspace_); + // Call this since it is possible the trash and zoom buttons need + // to move. e.g. on a bottom positioned flyout when zoom is clicked. + this.targetWorkspace_.resize(); } }; @@ -1093,7 +1095,9 @@ Blockly.Flyout.prototype.reflowVertical = function(blocks) { } // Record the width for .getMetrics_ and .position. this.width_ = flyoutWidth; - Blockly.resizeSvgContents(this.workspace_); + // Call this since it is possible the trash and zoom buttons need + // to move. e.g. on a bottom positioned flyout when zoom is clicked. + this.targetWorkspace_.resize(); } }; diff --git a/core/generator.js b/core/generator.js index 3cab1ccf3..3c6a5c7e1 100644 --- a/core/generator.js +++ b/core/generator.js @@ -70,6 +70,13 @@ Blockly.Generator.prototype.STATEMENT_PREFIX = null; */ Blockly.Generator.prototype.INDENT = ' '; +/** + * Maximum length for a comment before wrapping. Does not account for + * indenting level. + * @type {number} + */ +Blockly.Generator.prototype.COMMENT_WRAP = 60; + /** * Generate code for all blocks in the workspace to the specified language. * @param {Blockly.Workspace} workspace Workspace to generate code from. @@ -319,8 +326,8 @@ Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; */ Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) { if (!this.definitions_[desiredName]) { - var functionName = - this.variableDB_.getDistinctName(desiredName, this.NAME_TYPE); + var functionName = this.variableDB_.getDistinctName(desiredName, + Blockly.Procedures.NAME_TYPE); this.functionNames_[desiredName] = functionName; var codeText = code.join('\n').replace( this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); diff --git a/core/inject.js b/core/inject.js index d8e6e3d20..667a8713d 100644 --- a/core/inject.js +++ b/core/inject.js @@ -264,11 +264,12 @@ Blockly.init_ = function(mainWorkspace) { } }); - Blockly.bindEvent_(window, 'resize', null, - function() { - Blockly.hideChaff(true); - Blockly.svgResize(mainWorkspace); - }); + var workspaceResizeHandler = Blockly.bindEvent_(window, 'resize', null, + function() { + Blockly.hideChaff(true); + Blockly.svgResize(mainWorkspace); + }); + mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler); Blockly.inject.bindDocumentEvents_(); diff --git a/core/options.js b/core/options.js index 4254fe859..c86224cfc 100644 --- a/core/options.js +++ b/core/options.js @@ -31,7 +31,7 @@ goog.provide('Blockly.Options'); * Parse the user-specified options, using reasonable defaults where behaviour * is unspecified. * @param {!Object} options Dictionary of options. Specification: - * https://developers.google.com/blockly/installation/overview#configuration + * https://developers.google.com/blockly/guides/get-started/web#configuration * @constructor */ Blockly.Options = function(options) { @@ -146,7 +146,7 @@ Blockly.Options.prototype.getMetrics = function() { return null; }; /** * Parse the user-specified zoom options, using reasonable defaults where * behaviour is unspecified. See zoom documentation: - * https://developers.google.com/blockly/installation/zoom + * https://developers.google.com/blockly/guides/configure/web/zoom * @param {!Object} options Dictionary of options. * @return {!Object} A dictionary of normalized options. * @private @@ -190,7 +190,7 @@ Blockly.Options.parseZoomOptions_ = function(options) { /** * Parse the user-specified grid options, using reasonable defaults where * behaviour is unspecified. See grid documentation: - * https://developers.google.com/blockly/installation/grid + * https://developers.google.com/blockly/guides/configure/web/grid * @param {!Object} options Dictionary of options. * @return {!Object} A dictionary of normalized options. * @private diff --git a/core/tooltip.js b/core/tooltip.js index b4ded3772..3aa828812 100644 --- a/core/tooltip.js +++ b/core/tooltip.js @@ -239,7 +239,7 @@ Blockly.Tooltip.show_ = function() { while (goog.isFunction(tip)) { tip = tip(); } - tip = Blockly.Tooltip.wrap_(tip, Blockly.Tooltip.LIMIT); + tip = Blockly.utils.wrap(tip, Blockly.Tooltip.LIMIT); // Create new text, line by line. var lines = tip.split('\n'); for (var i = 0; i < lines.length; i++) { @@ -282,157 +282,3 @@ Blockly.Tooltip.show_ = function() { Blockly.Tooltip.DIV.style.top = anchorY + 'px'; Blockly.Tooltip.DIV.style.left = anchorX + 'px'; }; - -/** - * Wrap text to the specified width. - * @param {string} text Text to wrap. - * @param {number} limit Width to wrap each line. - * @return {string} Wrapped text. - * @private - */ -Blockly.Tooltip.wrap_ = function(text, limit) { - if (text.length <= limit) { - // Short text, no need to wrap. - return text; - } - // Split the text into words. - var words = text.trim().split(/\s+/); - // Set limit to be the length of the largest word. - for (var i = 0; i < words.length; i++) { - if (words[i].length > limit) { - limit = words[i].length; - } - } - - var lastScore; - var score = -Infinity; - var lastText; - var lineCount = 1; - do { - lastScore = score; - lastText = text; - // Create a list of booleans representing if a space (false) or - // a break (true) appears after each word. - var wordBreaks = []; - // Seed the list with evenly spaced linebreaks. - var steps = words.length / lineCount; - var insertedBreaks = 1; - for (var i = 0; i < words.length - 1; i++) { - if (insertedBreaks < (i + 1.5) / steps) { - insertedBreaks++; - wordBreaks[i] = true; - } else { - wordBreaks[i] = false; - } - } - wordBreaks = Blockly.Tooltip.wrapMutate_(words, wordBreaks, limit); - score = Blockly.Tooltip.wrapScore_(words, wordBreaks, limit); - text = Blockly.Tooltip.wrapToText_(words, wordBreaks); - lineCount++; - } while (score > lastScore); - return lastText; -}; - -/** - * Compute a score for how good the wrapping is. - * @param {!Array.} words Array of each word. - * @param {!Array.} wordBreaks Array of line breaks. - * @param {number} limit Width to wrap each line. - * @return {number} Larger the better. - * @private - */ -Blockly.Tooltip.wrapScore_ = function(words, wordBreaks, limit) { - // If this function becomes a performance liability, add caching. - // Compute the length of each line. - var lineLengths = [0]; - var linePunctuation = []; - for (var i = 0; i < words.length; i++) { - lineLengths[lineLengths.length - 1] += words[i].length; - if (wordBreaks[i] === true) { - lineLengths.push(0); - linePunctuation.push(words[i].charAt(words[i].length - 1)); - } else if (wordBreaks[i] === false) { - lineLengths[lineLengths.length - 1]++; - } - } - var maxLength = Math.max.apply(Math, lineLengths); - - var score = 0; - for (var i = 0; i < lineLengths.length; i++) { - // Optimize for width. - // -2 points per char over limit (scaled to the power of 1.5). - score -= Math.pow(Math.abs(limit - lineLengths[i]), 1.5) * 2; - // Optimize for even lines. - // -1 point per char smaller than max (scaled to the power of 1.5). - score -= Math.pow(maxLength - lineLengths[i], 1.5); - // Optimize for structure. - // Add score to line endings after punctuation. - if ('.?!'.indexOf(linePunctuation[i]) != -1) { - score += limit / 3; - } else if (',;)]}'.indexOf(linePunctuation[i]) != -1) { - score += limit / 4; - } - } - // All else being equal, the last line should not be longer than the - // previous line. For example, this looks wrong: - // aaa bbb - // ccc ddd eee - if (lineLengths.length > 1 && lineLengths[lineLengths.length - 1] <= - lineLengths[lineLengths.length - 2]) { - score += 0.5; - } - return score; -}; - -/** - * Mutate the array of line break locations until an optimal solution is found. - * No line breaks are added or deleted, they are simply moved around. - * @param {!Array.} words Array of each word. - * @param {!Array.} wordBreaks Array of line breaks. - * @param {number} limit Width to wrap each line. - * @return {!Array.} New array of optimal line breaks. - * @private - */ -Blockly.Tooltip.wrapMutate_ = function(words, wordBreaks, limit) { - var bestScore = Blockly.Tooltip.wrapScore_(words, wordBreaks, limit); - var bestBreaks; - // Try shifting every line break forward or backward. - for (var i = 0; i < wordBreaks.length - 1; i++) { - if (wordBreaks[i] == wordBreaks[i + 1]) { - continue; - } - var mutatedWordBreaks = [].concat(wordBreaks); - mutatedWordBreaks[i] = !mutatedWordBreaks[i]; - mutatedWordBreaks[i + 1] = !mutatedWordBreaks[i + 1]; - var mutatedScore = - Blockly.Tooltip.wrapScore_(words, mutatedWordBreaks, limit); - if (mutatedScore > bestScore) { - bestScore = mutatedScore; - bestBreaks = mutatedWordBreaks; - } - } - if (bestBreaks) { - // Found an improvement. See if it may be improved further. - return Blockly.Tooltip.wrapMutate_(words, bestBreaks, limit); - } - // No improvements found. Done. - return wordBreaks; -}; - -/** - * Reassemble the array of words into text, with the specified line breaks. - * @param {!Array.} words Array of each word. - * @param {!Array.} wordBreaks Array of line breaks. - * @return {string} Plain text. - * @private - */ -Blockly.Tooltip.wrapToText_ = function(words, wordBreaks) { - var text = []; - for (var i = 0; i < words.length; i++) { - text.push(words[i]); - if (wordBreaks[i] !== undefined) { - text.push(wordBreaks[i] ? '\n' : ' '); - } - } - return text.join(''); -}; diff --git a/core/utils.js b/core/utils.js index 1a0fba1b6..9b9e04d60 100644 --- a/core/utils.js +++ b/core/utils.js @@ -495,3 +495,171 @@ Blockly.genUid = function() { */ Blockly.genUid.soup_ = '!#%()*+,-./:;=?@[]^_`{|}~' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + +/** + * Wrap text to the specified width. + * @param {string} text Text to wrap. + * @param {number} limit Width to wrap each line. + * @return {string} Wrapped text. + */ +Blockly.utils.wrap = function(text, limit) { + var lines = text.split('\n'); + for (var i = 0; i < lines.length; i++) { + lines[i] = Blockly.utils.wrap_line_(lines[i], limit); + } + return lines.join('\n'); +}; + +/** + * Wrap single line of text to the specified width. + * @param {string} text Text to wrap. + * @param {number} limit Width to wrap each line. + * @return {string} Wrapped text. + * @private + */ +Blockly.utils.wrap_line_ = function(text, limit) { + if (text.length <= limit) { + // Short text, no need to wrap. + return text; + } + // Split the text into words. + var words = text.trim().split(/\s+/); + // Set limit to be the length of the largest word. + for (var i = 0; i < words.length; i++) { + if (words[i].length > limit) { + limit = words[i].length; + } + } + + var lastScore; + var score = -Infinity; + var lastText; + var lineCount = 1; + do { + lastScore = score; + lastText = text; + // Create a list of booleans representing if a space (false) or + // a break (true) appears after each word. + var wordBreaks = []; + // Seed the list with evenly spaced linebreaks. + var steps = words.length / lineCount; + var insertedBreaks = 1; + for (var i = 0; i < words.length - 1; i++) { + if (insertedBreaks < (i + 1.5) / steps) { + insertedBreaks++; + wordBreaks[i] = true; + } else { + wordBreaks[i] = false; + } + } + wordBreaks = Blockly.utils.wrapMutate_(words, wordBreaks, limit); + score = Blockly.utils.wrapScore_(words, wordBreaks, limit); + text = Blockly.utils.wrapToText_(words, wordBreaks); + lineCount++; + } while (score > lastScore); + return lastText; +}; + +/** + * Compute a score for how good the wrapping is. + * @param {!Array.} words Array of each word. + * @param {!Array.} wordBreaks Array of line breaks. + * @param {number} limit Width to wrap each line. + * @return {number} Larger the better. + * @private + */ +Blockly.utils.wrapScore_ = function(words, wordBreaks, limit) { + // If this function becomes a performance liability, add caching. + // Compute the length of each line. + var lineLengths = [0]; + var linePunctuation = []; + for (var i = 0; i < words.length; i++) { + lineLengths[lineLengths.length - 1] += words[i].length; + if (wordBreaks[i] === true) { + lineLengths.push(0); + linePunctuation.push(words[i].charAt(words[i].length - 1)); + } else if (wordBreaks[i] === false) { + lineLengths[lineLengths.length - 1]++; + } + } + var maxLength = Math.max.apply(Math, lineLengths); + + var score = 0; + for (var i = 0; i < lineLengths.length; i++) { + // Optimize for width. + // -2 points per char over limit (scaled to the power of 1.5). + score -= Math.pow(Math.abs(limit - lineLengths[i]), 1.5) * 2; + // Optimize for even lines. + // -1 point per char smaller than max (scaled to the power of 1.5). + score -= Math.pow(maxLength - lineLengths[i], 1.5); + // Optimize for structure. + // Add score to line endings after punctuation. + if ('.?!'.indexOf(linePunctuation[i]) != -1) { + score += limit / 3; + } else if (',;)]}'.indexOf(linePunctuation[i]) != -1) { + score += limit / 4; + } + } + // All else being equal, the last line should not be longer than the + // previous line. For example, this looks wrong: + // aaa bbb + // ccc ddd eee + if (lineLengths.length > 1 && lineLengths[lineLengths.length - 1] <= + lineLengths[lineLengths.length - 2]) { + score += 0.5; + } + return score; +}; + +/** + * Mutate the array of line break locations until an optimal solution is found. + * No line breaks are added or deleted, they are simply moved around. + * @param {!Array.} words Array of each word. + * @param {!Array.} wordBreaks Array of line breaks. + * @param {number} limit Width to wrap each line. + * @return {!Array.} New array of optimal line breaks. + * @private + */ +Blockly.utils.wrapMutate_ = function(words, wordBreaks, limit) { + var bestScore = Blockly.utils.wrapScore_(words, wordBreaks, limit); + var bestBreaks; + // Try shifting every line break forward or backward. + for (var i = 0; i < wordBreaks.length - 1; i++) { + if (wordBreaks[i] == wordBreaks[i + 1]) { + continue; + } + var mutatedWordBreaks = [].concat(wordBreaks); + mutatedWordBreaks[i] = !mutatedWordBreaks[i]; + mutatedWordBreaks[i + 1] = !mutatedWordBreaks[i + 1]; + var mutatedScore = + Blockly.utils.wrapScore_(words, mutatedWordBreaks, limit); + if (mutatedScore > bestScore) { + bestScore = mutatedScore; + bestBreaks = mutatedWordBreaks; + } + } + if (bestBreaks) { + // Found an improvement. See if it may be improved further. + return Blockly.utils.wrapMutate_(words, bestBreaks, limit); + } + // No improvements found. Done. + return wordBreaks; +}; + +/** + * Reassemble the array of words into text, with the specified line breaks. + * @param {!Array.} words Array of each word. + * @param {!Array.} wordBreaks Array of line breaks. + * @return {string} Plain text. + * @private + */ +Blockly.utils.wrapToText_ = function(words, wordBreaks) { + var text = []; + for (var i = 0; i < words.length; i++) { + text.push(words[i]); + if (wordBreaks[i] !== undefined) { + text.push(wordBreaks[i] ? '\n' : ' '); + } + } + return text.join(''); +}; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 52560ffdc..de636a7a2 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -64,6 +64,12 @@ Blockly.WorkspaceSvg = function(options) { }; goog.inherits(Blockly.WorkspaceSvg, Blockly.Workspace); +/** + * Wrapper function called when a resize event occurs. + * @type {Array.} Data that can be passed to unbindEvent_ + */ +Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null; + /** * Svg workspaces are user-visible (as opposed to a headless workspace). * @type {boolean} True if visible. False if headless. @@ -138,6 +144,14 @@ Blockly.WorkspaceSvg.prototype.scrollbar = null; */ Blockly.WorkspaceSvg.prototype.lastSound_ = null; +/** + * Save resize handler data so we can delete it later in dispose. + * @param {!Array.} handler Data that can be passed to unbindEvent_. + */ +Blockly.WorkspaceSvg.prototype.setResizeHandlerWrapper = function(handler) { + this.resizeHandlerWrapper_ = handler; +}; + /** * Create the workspace DOM elements. * @param {string=} opt_backgroundClass Either 'blocklyMainBackground' or @@ -238,6 +252,10 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { // Top-most workspace. Dispose of the SVG too. goog.dom.removeNode(this.getParentSvg()); } + if (this.resizeHandlerWrapper_) { + Blockly.unbindEvent_(this.resizeHandlerWrapper_); + this.resizeHandlerWrapper_ = null; + } }; /** @@ -932,7 +950,7 @@ Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) { var sound = this.SOUNDS_[name]; if (sound) { // Don't play one sound on top of another. - var now = new Date(); + var now = new Date; if (now - this.lastSound_ < Blockly.SOUND_LIMIT) { return; } diff --git a/core/xml.js b/core/xml.js index f2664ac76..70fd915a5 100644 --- a/core/xml.js +++ b/core/xml.js @@ -294,7 +294,11 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) { for (var i = 0; i < childCount; i++) { var xmlChild = xml.childNodes[i]; var name = xmlChild.nodeName.toLowerCase(); - if (name == 'block') { + if (name == 'block' || + (name == 'shadow' && !Blockly.Events.recordUndo)) { + // Allow top-level shadow blocks if recordUndo is disabled since + // that means an undo is in progress. Such a block is expected + // to be moved to a nested destination in the next operation. var block = Blockly.Xml.domToBlock(xmlChild, workspace); var blockX = parseInt(xmlChild.getAttribute('x'), 10); var blockY = parseInt(xmlChild.getAttribute('y'), 10); diff --git a/dart_compressed.js b/dart_compressed.js index 57b29d82f..963bbc380 100644 --- a/dart_compressed.js +++ b/dart_compressed.js @@ -9,7 +9,8 @@ Blockly.Dart.ORDER_ASSIGNMENT=15;Blockly.Dart.ORDER_NONE=99; Blockly.Dart.init=function(a){Blockly.Dart.definitions_=Object.create(null);Blockly.Dart.functionNames_=Object.create(null);Blockly.Dart.variableDB_?Blockly.Dart.variableDB_.reset():Blockly.Dart.variableDB_=new Blockly.Names(Blockly.Dart.RESERVED_WORDS_);var b=[];a=Blockly.Variables.allVariables(a);if(a.length){for(var c=0;c= ")+d+"; "+b;b=Math.abs(parseFloat(e));a=(1==b?a+(g?"++":"--"):a+((g?" += ":" -= ")+b))+(") {\n"+f+"}\n")}else a="",g=c,c.match(/^\w+$/)||Blockly.isNumber(c)||(g=Blockly.Dart.variableDB_.getDistinctName(b+"_start",Blockly.Variables.NAME_TYPE),a+="var "+g+" = "+c+";\n"),c=d,d.match(/^\w+$/)||Blockly.isNumber(d)||(c=Blockly.Dart.variableDB_.getDistinctName(b+"_end",Blockly.Variables.NAME_TYPE),a+="var "+c+" = "+d+";\n"),d=Blockly.Dart.variableDB_.getDistinctName(b+ -"_inc",Blockly.Variables.NAME_TYPE),a+="num "+d+" = ",a=Blockly.isNumber(e)?a+(Math.abs(e)+";\n"):a+("("+e+").abs();\n"),a=a+("if ("+g+" > "+c+") {\n")+(Blockly.Dart.INDENT+d+" = -"+d+";\n"),a+="}\n",a+="for ("+b+" = "+g+";\n "+d+" >= 0 ? "+b+" <= "+c+" : "+b+" >= "+c+";\n "+b+" += "+d+") {\n"+f+"}\n";return a}; +"_inc",Blockly.Variables.NAME_TYPE),a+="num "+d+" = ",a=Blockly.isNumber(e)?a+(Math.abs(e)+";\n"):a+("("+e+").abs();\n"),a=a+("if ("+g+" > "+c+") {\n")+(Blockly.Dart.INDENT+d+" = -"+d+";\n"),a+="}\n",a+="for ("+b+" = "+g+"; "+d+" >= 0 ? "+b+" <= "+c+" : "+b+" >= "+c+"; "+b+" += "+d+") {\n"+f+"}\n";return a}; Blockly.Dart.controls_forEach=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE),c=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_ASSIGNMENT)||"[]",d=Blockly.Dart.statementToCode(a,"DO"),d=Blockly.Dart.addLoopTrap(d,a.id);return"for (var "+b+" in "+c+") {\n"+d+"}\n"}; Blockly.Dart.controls_flow_statements=function(a){switch(a.getFieldValue("FLOW")){case "BREAK":return"break;\n";case "CONTINUE":return"continue;\n"}throw"Unknown flow statement.";};Blockly.Dart.math={};Blockly.Dart.addReservedWords("Math");Blockly.Dart.math_number=function(a){a=parseFloat(a.getFieldValue("NUM"));var b;Infinity==a?(a="double.INFINITY",b=Blockly.Dart.ORDER_UNARY_POSTFIX):-Infinity==a?(a="-double.INFINITY",b=Blockly.Dart.ORDER_UNARY_PREFIX):b=0>a?Blockly.Dart.ORDER_UNARY_PREFIX:Blockly.Dart.ORDER_ATOMIC;return[a,b]}; Blockly.Dart.math_arithmetic=function(a){var b={ADD:[" + ",Blockly.Dart.ORDER_ADDITIVE],MINUS:[" - ",Blockly.Dart.ORDER_ADDITIVE],MULTIPLY:[" * ",Blockly.Dart.ORDER_MULTIPLICATIVE],DIVIDE:[" / ",Blockly.Dart.ORDER_MULTIPLICATIVE],POWER:[null,Blockly.Dart.ORDER_NONE]}[a.getFieldValue("OP")],c=b[0],b=b[1],d=Blockly.Dart.valueToCode(a,"A",b)||"0";a=Blockly.Dart.valueToCode(a,"B",b)||"0";return c?[d+c+a,b]:(Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;",["Math.pow("+d+", "+a+ @@ -55,7 +56,7 @@ Blockly.Dart.math_number_property=function(a){var b=Blockly.Dart.valueToCode(a," Blockly.Dart.math_change=function(a){var b=Blockly.Dart.valueToCode(a,"DELTA",Blockly.Dart.ORDER_ADDITIVE)||"0";a=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE);return a+" = ("+a+" is num ? "+a+" : 0) + "+b+";\n"};Blockly.Dart.math_round=Blockly.Dart.math_single;Blockly.Dart.math_trig=Blockly.Dart.math_single; Blockly.Dart.math_on_list=function(a){var b=a.getFieldValue("OP");a=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_NONE)||"[]";switch(b){case "SUM":b=Blockly.Dart.provideFunction_("math_sum",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," num sumVal = 0;"," myList.forEach((num entry) {sumVal += entry;});"," return sumVal;","}"]);b=b+"("+a+")";break;case "MIN":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_min", ["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," if (myList.isEmpty) return null;"," num minVal = myList[0];"," myList.forEach((num entry) {minVal = Math.min(minVal, entry);});"," return minVal;","}"]);b=b+"("+a+")";break;case "MAX":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_max",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," if (myList.isEmpty) return null;"," num maxVal = myList[0];", -" myList.forEach((num entry) {maxVal = Math.max(maxVal, entry);});"," return maxVal;","}"]);b=b+"("+a+")";break;case "AVERAGE":b=Blockly.Dart.provideFunction_("math_average",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only."," List localList = new List.from(myList);"," localList.removeMatching((a) => a is! num);"," if (localList.isEmpty) return null;"," num sumVal = 0;"," localList.forEach((num entry) {sumVal += entry;});"," return sumVal / localList.length;", +" myList.forEach((num entry) {maxVal = Math.max(maxVal, entry);});"," return maxVal;","}"]);b=b+"("+a+")";break;case "AVERAGE":b=Blockly.Dart.provideFunction_("math_mean",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only."," List localList = new List.from(myList);"," localList.removeMatching((a) => a is! num);"," if (localList.isEmpty) return null;"," num sumVal = 0;"," localList.forEach((num entry) {sumVal += entry;});"," return sumVal / localList.length;", "}"]);b=b+"("+a+")";break;case "MEDIAN":b=Blockly.Dart.provideFunction_("math_median",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only, then sort, then return middle value"," // or the average of two middle values if list has an even number of elements."," List localList = new List.from(myList);"," localList.removeMatching((a) => a is! num);"," if (localList.isEmpty) return null;"," localList.sort((a, b) => (a - b));"," int index = localList.length ~/ 2;", " if (localList.length % 2 == 1) {"," return localList[index];"," } else {"," return (localList[index - 1] + localList[index]) / 2;"," }","}"]);b=b+"("+a+")";break;case "MODE":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_modes",["List "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List values) {"," List modes = [];"," List counts = [];"," int maxCount = 0;"," for (int i = 0; i < values.length; i++) {"," var value = values[i];", " bool found = false;"," int thisCount;"," for (int j = 0; j < counts.length; j++) {"," if (counts[j][0] == value) {"," thisCount = ++counts[j][1];"," found = true;"," break;"," }"," }"," if (!found) {"," counts.add([value, 1]);"," thisCount = 1;"," }"," maxCount = Math.max(thisCount, maxCount);"," }"," for (int j = 0; j < counts.length; j++) {"," if (counts[j][1] == maxCount) {"," modes.add(counts[j][0]);"," }"," }"," return modes;", @@ -66,13 +67,13 @@ Blockly.Dart.math_constrain=function(a){Blockly.Dart.definitions_.import_dart_ma Blockly.Dart.math_random_int=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";var b=Blockly.Dart.valueToCode(a,"FROM",Blockly.Dart.ORDER_NONE)||"0";a=Blockly.Dart.valueToCode(a,"TO",Blockly.Dart.ORDER_NONE)||"0";return[Blockly.Dart.provideFunction_("math_random_int",["int "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(num a, num b) {"," if (a > b) {"," // Swap a and b to ensure a is smaller."," num c = a;"," a = b;"," b = c;"," }"," return new Math.Random().nextInt(b - a + 1) + a;", "}"])+"("+b+", "+a+")",Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.math_random_float=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";return["new Math.Random().nextDouble()",Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.procedures={}; Blockly.Dart.procedures_defreturn=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("NAME"),Blockly.Procedures.NAME_TYPE),c=Blockly.Dart.statementToCode(a,"STACK");Blockly.Dart.STATEMENT_PREFIX&&(c=Blockly.Dart.prefixLines(Blockly.Dart.STATEMENT_PREFIX.replace(/%1/g,"'"+a.id+"'"),Blockly.Dart.INDENT)+c);Blockly.Dart.INFINITE_LOOP_TRAP&&(c=Blockly.Dart.INFINITE_LOOP_TRAP.replace(/%1/g,"'"+a.id+"'")+c);var d=Blockly.Dart.valueToCode(a,"RETURN",Blockly.Dart.ORDER_NONE)||"";d&&(d=" return "+ -d+";\n");for(var e=d?"dynamic":"void",f=[],g=0;g + - - - - - - + + + + + + + +