diff --git a/.eslintignore b/.eslintignore index 792c074e6..373bf7a87 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,6 +2,7 @@ *_uncompressed*.js gulpfile.js /msg/* +/dist/* /core/utils/global.js /tests/blocks/* /tests/themes/* diff --git a/blockly_compressed.js b/blockly_compressed.js index 212e8653f..3f2e0b4b2 100644 --- a/blockly_compressed.js +++ b/blockly_compressed.js @@ -1,5 +1,16 @@ // Do not edit this file; automatically generated by gulp. -'use strict';var Blockly={constants:{},LINE_MODE_MULTIPLIER:40,PAGE_MODE_MULTIPLIER:125,DRAG_RADIUS:5,FLYOUT_DRAG_RADIUS:10,SNAP_RADIUS:28};Blockly.CONNECTING_SNAP_RADIUS=Blockly.SNAP_RADIUS;Blockly.CURRENT_CONNECTION_PREFERENCE=8;Blockly.BUMP_DELAY=250;Blockly.BUMP_RANDOMNESS=10;Blockly.COLLAPSE_CHARS=30;Blockly.LONGPRESS=750;Blockly.SOUND_LIMIT=100;Blockly.DRAG_STACK=!0;Blockly.HSV_SATURATION=.45;Blockly.HSV_VALUE=.65;Blockly.SPRITE={width:96,height:124,url:"sprites.png"};Blockly.INPUT_VALUE=1; + +/* eslint-disable */ +;(function(root, factory) { + if (typeof define === 'function' && define.amd) { // AMD + define([], factory); + } else if (typeof exports === 'object') { // Node.js + module.exports = factory(); + } else { // Browser + root.Blockly = factory(); + } +}(this, function() { + 'use strict';var Blockly={constants:{},LINE_MODE_MULTIPLIER:40,PAGE_MODE_MULTIPLIER:125,DRAG_RADIUS:5,FLYOUT_DRAG_RADIUS:10,SNAP_RADIUS:28};Blockly.CONNECTING_SNAP_RADIUS=Blockly.SNAP_RADIUS;Blockly.CURRENT_CONNECTION_PREFERENCE=8;Blockly.BUMP_DELAY=250;Blockly.BUMP_RANDOMNESS=10;Blockly.COLLAPSE_CHARS=30;Blockly.LONGPRESS=750;Blockly.SOUND_LIMIT=100;Blockly.DRAG_STACK=!0;Blockly.HSV_SATURATION=.45;Blockly.HSV_VALUE=.65;Blockly.SPRITE={width:96,height:124,url:"sprites.png"};Blockly.INPUT_VALUE=1; Blockly.OUTPUT_VALUE=2;Blockly.NEXT_STATEMENT=3;Blockly.PREVIOUS_STATEMENT=4;Blockly.DUMMY_INPUT=5;Blockly.ALIGN_LEFT=-1;Blockly.ALIGN_CENTRE=0;Blockly.ALIGN_RIGHT=1;Blockly.DRAG_NONE=0;Blockly.DRAG_STICKY=1;Blockly.DRAG_BEGIN=1;Blockly.DRAG_FREE=2;Blockly.OPPOSITE_TYPE=[];Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE]=Blockly.OUTPUT_VALUE;Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE]=Blockly.INPUT_VALUE;Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT]=Blockly.PREVIOUS_STATEMENT; Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT]=Blockly.NEXT_STATEMENT;Blockly.TOOLBOX_AT_TOP=0;Blockly.TOOLBOX_AT_BOTTOM=1;Blockly.TOOLBOX_AT_LEFT=2;Blockly.TOOLBOX_AT_RIGHT=3;Blockly.DELETE_AREA_NONE=null;Blockly.DELETE_AREA_TRASH=1;Blockly.DELETE_AREA_TOOLBOX=2;Blockly.VARIABLE_CATEGORY_NAME="VARIABLE";Blockly.VARIABLE_DYNAMIC_CATEGORY_NAME="VARIABLE_DYNAMIC";Blockly.PROCEDURE_CATEGORY_NAME="PROCEDURE";Blockly.RENAME_VARIABLE_ID="RENAME_VARIABLE_ID";Blockly.DELETE_VARIABLE_ID="DELETE_VARIABLE_ID";Blockly.utils={};Blockly.utils.global=function(){return"object"===typeof self?self:"object"===typeof window?window:"object"===typeof global?global:this}();Blockly.Msg={};Blockly.utils.global.Blockly||(Blockly.utils.global.Blockly={});Blockly.utils.global.Blockly.Msg||(Blockly.utils.global.Blockly.Msg=Blockly.Msg);Blockly.utils.colour={}; Blockly.utils.colour.parse=function(a){a=String(a).toLowerCase().trim();var b=Blockly.utils.colour.names[a];if(b)return b;b="0x"==a.substring(0,2)?"#"+a.substring(2):a;b="#"==b[0]?b:"#"+b;if(/^#[0-9a-f]{6}$/.test(b))return b;if(/^#[0-9a-f]{3}$/.test(b))return["#",b[1],b[1],b[2],b[2],b[3],b[3]].join("");var c=a.match(/^(?:rgb)?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);return c&&(a=Number(c[1]),b=Number(c[2]),c=Number(c[3]),0<=a&&256>a&&0<=b&&256>b&&0<=c&&256>c)?Blockly.utils.colour.rgbToHex(a,b, @@ -7,7 +18,7 @@ c):null};Blockly.utils.colour.rgbToHex=function(a,b,c){b=a<<16|b<<8|c;return 16> Blockly.utils.colour.hsvToHex=function(a,b,c){var d=0,e=0,f=0;if(0==b)f=e=d=c;else{var g=Math.floor(a/60),h=a/60-g;a=c*(1-b);var k=c*(1-b*h);b=c*(1-b*(1-h));switch(g){case 1:d=k;e=c;f=a;break;case 2:d=a;e=c;f=b;break;case 3:d=a;e=k;f=c;break;case 4:d=b;e=a;f=c;break;case 5:d=c;e=a;f=k;break;case 6:case 0:d=c,e=b,f=a}}return Blockly.utils.colour.rgbToHex(Math.floor(d),Math.floor(e),Math.floor(f))}; Blockly.utils.colour.blend=function(a,b,c){a=Blockly.utils.colour.parse(a);if(!a)return null;b=Blockly.utils.colour.parse(b);if(!b)return null;a=Blockly.utils.colour.hexToRgb(a);b=Blockly.utils.colour.hexToRgb(b);return Blockly.utils.colour.rgbToHex(Math.round(b[0]+c*(a[0]-b[0])),Math.round(b[1]+c*(a[1]-b[1])),Math.round(b[2]+c*(a[2]-b[2])))}; Blockly.utils.colour.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00"};Blockly.utils.Coordinate=function(a,b){this.x=a;this.y=b};Blockly.utils.Coordinate.equals=function(a,b){return a==b?!0:a&&b?a.x==b.x&&a.y==b.y:!1};Blockly.utils.Coordinate.distance=function(a,b){var c=a.x-b.x;a=a.y-b.y;return Math.sqrt(c*c+a*a)};Blockly.utils.Coordinate.magnitude=function(a){return Math.sqrt(a.x*a.x+a.y*a.y)};Blockly.utils.Coordinate.difference=function(a,b){return new Blockly.utils.Coordinate(a.x-b.x,a.y-b.y)}; -Blockly.utils.Coordinate.sum=function(a,b){return new Blockly.utils.Coordinate(a.x+b.x,a.y+b.y)};Blockly.utils.Coordinate.prototype.scale=function(a){this.x*=a;this.y*=a;return this};Blockly.utils.Coordinate.prototype.translate=function(a,b){this.x+=a;this.y+=b;return this};Blockly.utils.string={};Blockly.utils.string.startsWith=function(a,b){return 0==a.lastIndexOf(b,0)};Blockly.utils.string.shortestStringLength=function(a){return a.length?a.reduce(function(a,c){return a.length=this.left&&a<=this.right&&b>=this.top&&b<=this.bottom};Blockly.utils.string={};Blockly.utils.string.startsWith=function(a,b){return 0==a.lastIndexOf(b,0)};Blockly.utils.string.shortestStringLength=function(a){return a.length?a.reduce(function(a,c){return a.lengthb&&(b=c[d].length);d=-Infinity;var e=1;do{var f=d;var g=a;a=[];var h=c.length/e,k=1;for(d=0;df);return g}; @@ -32,7 +43,7 @@ Blockly.utils.tokenizeInterpolation_=function(a,b){var c=[],d=a.split("");d.push Blockly.utils.genUid=function(){for(var a=Blockly.utils.genUid.soup_.length,b=[],c=0;20>c;c++)b[c]=Blockly.utils.genUid.soup_.charAt(Math.random()*a);return b.join("")};Blockly.utils.genUid.soup_="!#$%()*+,-./:;=?@[]^_`{|}~ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; Blockly.utils.is3dSupported=function(){if(void 0!==Blockly.utils.is3dSupported.cached_)return Blockly.utils.is3dSupported.cached_;if(!Blockly.utils.global.getComputedStyle)return!1;var a=document.createElement("p"),b="none",c={webkitTransform:"-webkit-transform",OTransform:"-o-transform",msTransform:"-ms-transform",MozTransform:"-moz-transform",transform:"transform"};document.body.insertBefore(a,null);for(var d in c)if(void 0!==a.style[d]){a.style[d]="translate3d(1px,1px,1px)";b=Blockly.utils.global.getComputedStyle(a); if(!b)return document.body.removeChild(a),!1;b=b.getPropertyValue(c[d])}document.body.removeChild(a);Blockly.utils.is3dSupported.cached_="none"!==b;return Blockly.utils.is3dSupported.cached_};Blockly.utils.runAfterPageLoad=function(a){if("object"!=typeof document)throw Error("Blockly.utils.runAfterPageLoad() requires browser document.");if("complete"==document.readyState)a();else var b=setInterval(function(){"complete"==document.readyState&&(clearInterval(b),a())},10)}; -Blockly.utils.getViewportBBox=function(){var a=Blockly.utils.style.getViewportPageOffset();return{right:document.documentElement.clientWidth+a.x,bottom:document.documentElement.clientHeight+a.y,top:a.y,left:a.x}};Blockly.utils.arrayRemove=function(a,b){b=a.indexOf(b);if(-1==b)return!1;a.splice(b,1);return!0}; +Blockly.utils.getViewportBBox=function(){var a=Blockly.utils.style.getViewportPageOffset();return new Blockly.utils.Rect(a.y,document.documentElement.clientHeight+a.y,a.x,document.documentElement.clientWidth+a.x)};Blockly.utils.arrayRemove=function(a,b){b=a.indexOf(b);if(-1==b)return!1;a.splice(b,1);return!0}; Blockly.utils.getDocumentScroll=function(){var a=document.documentElement,b=window;return Blockly.utils.userAgent.IE&&b.pageYOffset!=a.scrollTop?new Blockly.utils.Coordinate(a.scrollLeft,a.scrollTop):new Blockly.utils.Coordinate(b.pageXOffset||a.scrollLeft,b.pageYOffset||a.scrollTop)};Blockly.utils.getBlockTypeCounts=function(a,b){var c=Object.create(null),d=a.getDescendants(!0);b&&(a=a.getNextBlock())&&(a=d.indexOf(a),d.splice(a,d.length-a));for(a=0;b=d[a];a++)c[b.type]?c[b.type]++:c[b.type]=1;return c}; Blockly.utils.screenToWsCoordinates=function(a,b){var c=b.x;b=b.y;var d=a.getInjectionDiv().getBoundingClientRect();c=new Blockly.utils.Coordinate(c-d.left,b-d.top);b=a.getOriginOffsetInPixels();return Blockly.utils.Coordinate.difference(c,b).scale(1/a.scale)}; Blockly.utils.parseBlockColour=function(a){var b="string"==typeof a?Blockly.utils.replaceMessageReferences(a):a,c=Number(b);if(!isNaN(c)&&0<=c&&360>=c)return{hue:c,hex:Blockly.utils.colour.hsvToHex(c,Blockly.HSV_SATURATION,255*Blockly.HSV_VALUE)};if(c=Blockly.utils.colour.parse(b))return{hue:null,hex:c};c='Invalid colour: "'+b+'"';a!=b&&(c+=' (from "'+a+'")');throw Error(c);};Blockly.Events={};Blockly.Events.group_="";Blockly.Events.recordUndo=!0;Blockly.Events.disabled_=0;Blockly.Events.CREATE="create";Blockly.Events.BLOCK_CREATE=Blockly.Events.CREATE;Blockly.Events.DELETE="delete";Blockly.Events.BLOCK_DELETE=Blockly.Events.DELETE;Blockly.Events.CHANGE="change";Blockly.Events.BLOCK_CHANGE=Blockly.Events.CHANGE;Blockly.Events.MOVE="move";Blockly.Events.BLOCK_MOVE=Blockly.Events.MOVE;Blockly.Events.VAR_CREATE="var_create";Blockly.Events.VAR_DELETE="var_delete"; @@ -45,11 +56,11 @@ Blockly.Events.fromJson=function(a,b){switch(a.type){case Blockly.Events.CREATE: new Blockly.Events.VarRename(null,"");break;case Blockly.Events.UI:c=new Blockly.Events.Ui(null,"","","");break;case Blockly.Events.COMMENT_CREATE:c=new Blockly.Events.CommentCreate(null);break;case Blockly.Events.COMMENT_CHANGE:c=new Blockly.Events.CommentChange(null,"","");break;case Blockly.Events.COMMENT_MOVE:c=new Blockly.Events.CommentMove(null);break;case Blockly.Events.COMMENT_DELETE:c=new Blockly.Events.CommentDelete(null);break;case Blockly.Events.FINISHED_LOADING:c=new Blockly.Events.FinishedLoading(b); break;default:throw Error("Unknown event type.");}c.fromJson(a);c.workspaceId=b.id;return c}; Blockly.Events.disableOrphans=function(a){if((a.type==Blockly.Events.MOVE||a.type==Blockly.Events.CREATE)&&a.workspaceId){var b=Blockly.Workspace.getById(a.workspaceId);if(a=b.getBlockById(a.blockId)){var c=a.getParent();if(c&&c.isEnabled())for(b=a.getDescendants(!1),a=0;c=b[a];a++)c.setEnabled(!0);else if((a.outputConnection||a.previousConnection)&&!b.isDragging()){do a.setEnabled(!1),a=a.getNextBlock();while(a)}}}};Blockly.Events.Abstract=function(){this.workspaceId=void 0;this.group=Blockly.Events.getGroup();this.recordUndo=Blockly.Events.recordUndo};Blockly.Events.Abstract.prototype.toJson=function(){var a={type:this.type};this.group&&(a.group=this.group);return a};Blockly.Events.Abstract.prototype.fromJson=function(a){this.group=a.group};Blockly.Events.Abstract.prototype.isNull=function(){return!1};Blockly.Events.Abstract.prototype.run=function(a){}; -Blockly.Events.Abstract.prototype.getEventWorkspace_=function(){if(this.workspaceId)var a=Blockly.Workspace.getById(this.workspaceId);if(!a)throw Error("Workspace is null. Event must have been generated from real Blockly events.");return a};Blockly.utils.object={};Blockly.utils.object.inherits=function(a,b){a.superClass_=b.prototype;a.prototype=Object.create(b.prototype);a.prototype.constructor=a};Blockly.utils.object.mixin=function(a,b){for(var c in b)a[c]=b[c]};Blockly.utils.object.deepMerge=function(a,b){for(var c in b)a[c]="object"===typeof b[c]?Blockly.utils.object.deepMerge(a[c]||Object.create(null),b[c]):b[c];return a};Blockly.utils.object.values=function(a){return Object.values?Object.values(a):Object.keys(a).map(function(b){return a[b]})};Blockly.Events.Ui=function(a,b,c,d){Blockly.Events.Ui.superClass_.constructor.call(this);this.blockId=a?a.id:null;this.workspaceId=a?a.workspace.id:void 0;this.element=b;this.oldValue=c;this.newValue=d;this.recordUndo=!1};Blockly.utils.object.inherits(Blockly.Events.Ui,Blockly.Events.Abstract);Blockly.Events.Ui.prototype.type=Blockly.Events.UI; -Blockly.Events.Ui.prototype.toJson=function(){var a=Blockly.Events.Ui.superClass_.toJson.call(this);a.element=this.element;void 0!==this.newValue&&(a.newValue=this.newValue);this.blockId&&(a.blockId=this.blockId);return a};Blockly.Events.Ui.prototype.fromJson=function(a){Blockly.Events.Ui.superClass_.fromJson.call(this,a);this.element=a.element;this.newValue=a.newValue;this.blockId=a.blockId};Blockly.utils.dom={};Blockly.utils.dom.SVG_NS="http://www.w3.org/2000/svg";Blockly.utils.dom.HTML_NS="http://www.w3.org/1999/xhtml";Blockly.utils.dom.XLINK_NS="http://www.w3.org/1999/xlink";Blockly.utils.dom.Node={ELEMENT_NODE:1,TEXT_NODE:3,COMMENT_NODE:8,DOCUMENT_POSITION_CONTAINED_BY:16};Blockly.utils.dom.cacheWidths_=null;Blockly.utils.dom.cacheReference_=0;Blockly.utils.dom.canvasContext_=null; +Blockly.Events.Abstract.prototype.getEventWorkspace_=function(){if(this.workspaceId)var a=Blockly.Workspace.getById(this.workspaceId);if(!a)throw Error("Workspace is null. Event must have been generated from real Blockly events.");return a};Blockly.utils.object={};Blockly.utils.object.inherits=function(a,b){a.superClass_=b.prototype;a.prototype=Object.create(b.prototype);a.prototype.constructor=a};Blockly.utils.object.mixin=function(a,b){for(var c in b)a[c]=b[c]};Blockly.utils.object.deepMerge=function(a,b){for(var c in b)a[c]=null!=b[c]&&"object"===typeof b[c]?Blockly.utils.object.deepMerge(a[c]||Object.create(null),b[c]):b[c];return a};Blockly.utils.object.values=function(a){return Object.values?Object.values(a):Object.keys(a).map(function(b){return a[b]})};Blockly.Events.Ui=function(a,b,c,d){Blockly.Events.Ui.superClass_.constructor.call(this);this.blockId=a?a.id:null;this.workspaceId=a?a.workspace.id:void 0;this.element=b;this.oldValue=c;this.newValue=d;this.recordUndo=!1};Blockly.utils.object.inherits(Blockly.Events.Ui,Blockly.Events.Abstract);Blockly.Events.Ui.prototype.type=Blockly.Events.UI; +Blockly.Events.Ui.prototype.toJson=function(){var a=Blockly.Events.Ui.superClass_.toJson.call(this);a.element=this.element;void 0!==this.newValue&&(a.newValue=this.newValue);this.blockId&&(a.blockId=this.blockId);return a};Blockly.Events.Ui.prototype.fromJson=function(a){Blockly.Events.Ui.superClass_.fromJson.call(this,a);this.element=a.element;this.newValue=a.newValue;this.blockId=a.blockId};Blockly.utils.dom={};Blockly.utils.dom.SVG_NS="http://www.w3.org/2000/svg";Blockly.utils.dom.HTML_NS="http://www.w3.org/1999/xhtml";Blockly.utils.dom.XLINK_NS="http://www.w3.org/1999/xlink";Blockly.utils.dom.NodeType={ELEMENT_NODE:1,TEXT_NODE:3,COMMENT_NODE:8,DOCUMENT_POSITION_CONTAINED_BY:16};Blockly.utils.dom.cacheWidths_=null;Blockly.utils.dom.cacheReference_=0;Blockly.utils.dom.canvasContext_=null; Blockly.utils.dom.createSvgElement=function(a,b,c){a=document.createElementNS(Blockly.utils.dom.SVG_NS,a);for(var d in b)a.setAttribute(d,b[d]);document.body.runtimeStyle&&(a.runtimeStyle=a.currentStyle=a.style);c&&c.appendChild(a);return a};Blockly.utils.dom.addClass=function(a,b){var c=a.getAttribute("class")||"";if(-1!=(" "+c+" ").indexOf(" "+b+" "))return!1;c&&(c+=" ");a.setAttribute("class",c+b);return!0}; Blockly.utils.dom.removeClass=function(a,b){var c=a.getAttribute("class");if(-1==(" "+c+" ").indexOf(" "+b+" "))return!1;c=c.split(/\s+/);for(var d=0;db||b>this.getChildCount())throw Error(Blockly.Component.Error.CHILD_INDEX_OUT_OF_BOUNDS);this.childIndex_[a.getId()]=a;if(a.getParent()==this){var d=this.children_.indexOf(a);-1>>/g,a),a=document.createElement("style"),a.id="blockly-common-style",c=document.createTextNode(c),a.appendChild(c),document.head.insertBefore(a,document.head.firstChild))}};Blockly.Css.setCursor=function(a){console.warn("Deprecated call to Blockly.Css.setCursor. See https://github.com/google/blockly/issues/981 for context")}; +null,b!=a.getElement()&&c.insertBefore(a.getElement(),b)):c?(this.element_||this.createDom(),b=this.getChildAt(b+1),a.render_(this.getContentElement(),b?b.element_:null)):this.inDocument_&&!a.inDocument_&&a.element_&&a.element_.parentNode&&a.element_.parentNode.nodeType==Blockly.utils.dom.NodeType.ELEMENT_NODE&&a.enterDocument()};Blockly.Component.prototype.getContentElement=function(){return this.element_};Blockly.Component.prototype.hasChildren=function(){return 0!=this.children_.length}; +Blockly.Component.prototype.getChildCount=function(){return this.children_.length};Blockly.Component.prototype.getChild=function(a){return a?this.childIndex_[a]||null:null};Blockly.Component.prototype.getChildAt=function(a){return this.children_[a]||null};Blockly.Component.prototype.forEachChild=function(a,b){for(var c=0;c>>/g,a),a=document.createElement("style"),a.id="blockly-common-style",c=document.createTextNode(c),a.appendChild(c),document.head.insertBefore(a,document.head.firstChild))}};Blockly.Css.setCursor=function(a){console.warn("Deprecated call to Blockly.Css.setCursor. See issue #981 for context")}; Blockly.Css.CONTENT=[".blocklySvg {","background-color: #fff;","outline: none;","overflow: hidden;","position: absolute;","display: block;","}",".blocklyWidgetDiv {","display: none;","position: absolute;","z-index: 99999;","}",".injectionDiv {","height: 100%;","position: relative;","overflow: hidden;","touch-action: none;","}",".blocklyNonSelectable {","user-select: none;","-ms-user-select: none;","-webkit-user-select: none;","}",".blocklyWsDragSurface {","display: none;","position: absolute;","top: 0;", "left: 0;","}",".blocklyWsDragSurface.blocklyOverflowVisible {","overflow: visible;","}",".blocklyBlockDragSurface {","display: none;","position: absolute;","top: 0;","left: 0;","right: 0;","bottom: 0;","overflow: visible !important;","z-index: 50;","}",".blocklyBlockCanvas.blocklyCanvasTransitioning,",".blocklyBubbleCanvas.blocklyCanvasTransitioning {","transition: transform .5s;","}",".blocklyTooltipDiv {","background-color: #ffffc7;","border: 1px solid #ddc;","box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);", -"color: #000;","display: none;","font-family: sans-serif;","font-size: 9pt;","opacity: .9;","padding: 2px;","position: absolute;","z-index: 100000;","}",".blocklyDropDownDiv {","position: absolute;","left: 0;","top: 0;","z-index: 1000;","display: none;","border: 1px solid;","border-color: #dadce0;","background-color: #fff;","border-radius: 2px;","padding: 4px;","box-shadow: 0px 0px 3px 1px rgba(0,0,0,.3);","}",".blocklyDropDownDiv.focused {","box-shadow: 0px 0px 6px 1px rgba(0,0,0,.3);","}",".blocklyDropDownContent {", +"color: #000;","display: none;","font: 9pt sans-serif;","opacity: .9;","padding: 2px;","position: absolute;","z-index: 100000;","}",".blocklyDropDownDiv {","position: absolute;","left: 0;","top: 0;","z-index: 1000;","display: none;","border: 1px solid;","border-color: #dadce0;","background-color: #fff;","border-radius: 2px;","padding: 4px;","box-shadow: 0 0 3px 1px rgba(0,0,0,.3);","}",".blocklyDropDownDiv.blocklyFocused {","box-shadow: 0 0 6px 1px rgba(0,0,0,.3);","}",".blocklyDropDownContent {", "max-height: 300px;","overflow: auto;","overflow-x: hidden;","}",".blocklyDropDownArrow {","position: absolute;","left: 0;","top: 0;","width: 16px;","height: 16px;","z-index: -1;","background-color: inherit;","border-color: inherit;","}",".blocklyDropDownButton {","display: inline-block;","float: left;","padding: 0;","margin: 4px;","border-radius: 4px;","outline: none;","border: 1px solid;","transition: box-shadow .1s;","cursor: pointer;","}",".blocklyArrowTop {","border-top: 1px solid;","border-left: 1px solid;", "border-top-left-radius: 4px;","border-color: inherit;","}",".blocklyArrowBottom {","border-bottom: 1px solid;","border-right: 1px solid;","border-bottom-right-radius: 4px;","border-color: inherit;","}",".blocklyResizeSE {","cursor: se-resize;","fill: #aaa;","}",".blocklyResizeSW {","cursor: sw-resize;","fill: #aaa;","}",".blocklyResizeLine {","stroke: #515A5A;","stroke-width: 1;","}",".blocklyHighlightedConnectionPath {","fill: none;","stroke: #fc3;","stroke-width: 4px;","}",".blocklyPathLight {", "fill: none;","stroke-linecap: round;","stroke-width: 1;","}",".blocklySelected>.blocklyPathLight {","display: none;","}",".blocklyDraggable {",'cursor: url("<<>>/handopen.cur"), auto;',"cursor: grab;","cursor: -webkit-grab;","}",".blocklyDragging {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyDraggable:active {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyBlockDragSurface .blocklyDraggable {", 'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyDragging.blocklyDraggingDelete {",'cursor: url("<<>>/handdelete.cur"), auto;',"}",".blocklyDragging>.blocklyPath,",".blocklyDragging>.blocklyPathLight {","fill-opacity: .8;","stroke-opacity: .8;","}",".blocklyDragging>.blocklyPathDark {","display: none;","}",".blocklyDisabled>.blocklyPath {","fill-opacity: .5;","stroke-opacity: .5;","}",".blocklyDisabled>.blocklyPathLight,",".blocklyDisabled>.blocklyPathDark {", -"display: none;","}",".blocklyInsertionMarker>.blocklyPath,",".blocklyInsertionMarker>.blocklyPathLight,",".blocklyInsertionMarker>.blocklyPathDark {","fill-opacity: .2;","stroke: none","}",".blocklyMultilineText {","font-family: monospace;","}",".blocklyNonEditableText>text {","pointer-events: none;","}",".blocklyFlyout {","position: absolute;","z-index: 20;","}",".blocklyText text {","cursor: default;","}",".blocklySvg text, .blocklyBlockDragSurface text {","user-select: none;","-ms-user-select: none;", +"display: none;","}",".blocklyInsertionMarker>.blocklyPath,",".blocklyInsertionMarker>.blocklyPathLight,",".blocklyInsertionMarker>.blocklyPathDark {","fill-opacity: .2;","stroke: none;","}",".blocklyMultilineText {","font-family: monospace;","}",".blocklyNonEditableText>text {","pointer-events: none;","}",".blocklyFlyout {","position: absolute;","z-index: 20;","}",".blocklyText text {","cursor: default;","}",".blocklySvg text,",".blocklyBlockDragSurface text {","user-select: none;","-ms-user-select: none;", "-webkit-user-select: none;","cursor: inherit;","}",".blocklyHidden {","display: none;","}",".blocklyFieldDropdown:not(.blocklyHidden) {","display: block;","}",".blocklyIconGroup {","cursor: default;","}",".blocklyIconGroup:not(:hover),",".blocklyIconGroupReadonly {","opacity: .6;","}",".blocklyIconShape {","fill: #00f;","stroke: #fff;","stroke-width: 1px;","}",".blocklyIconSymbol {","fill: #fff;","}",".blocklyMinimalBody {","margin: 0;","padding: 0;","}",".blocklyHtmlInput {","border: none;","border-radius: 4px;", "height: 100%;","margin: 0;","outline: none;","padding: 0;","width: 100%;","text-align: center;","display: block;","box-sizing: border-box;","}",".blocklyHtmlInput::-ms-clear {","display: none;","}",".blocklyMainBackground {","stroke-width: 1;","stroke: #c6c6c6;","}",".blocklyMutatorBackground {","fill: #fff;","stroke: #ddd;","stroke-width: 1;","}",".blocklyFlyoutBackground {","fill: #ddd;","fill-opacity: .8;","}",".blocklyMainWorkspaceScrollbar {","z-index: 20;","}",".blocklyFlyoutScrollbar {","z-index: 30;", -"}",".blocklyScrollbarHorizontal, .blocklyScrollbarVertical {","position: absolute;","outline: none;","}",".blocklyScrollbarBackground {","opacity: 0;","}",".blocklyScrollbarHandle {","fill: #ccc;","}",".blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,",".blocklyScrollbarHandle:hover {","fill: #bbb;","}",".blocklyFlyout .blocklyScrollbarHandle {","fill: #bbb;","}",".blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,",".blocklyFlyout .blocklyScrollbarHandle:hover {", -"fill: #aaa;","}",".blocklyInvalidInput {","background: #faa;","}",".blocklyContextMenu {","border-radius: 4px;","max-height: 100%;","}",".blocklyDropdownMenu {","border-radius: 2px;","padding: 0 !important;","}",".blocklyWidgetDiv .blocklyDropdownMenu .goog-menuitem,",".blocklyDropDownDiv .blocklyDropdownMenu .goog-menuitem {","padding-left: 28px;","}",".blocklyWidgetDiv .blocklyDropdownMenu .goog-menuitem.goog-menuitem-rtl,",".blocklyDropDownDiv .blocklyDropdownMenu .goog-menuitem.goog-menuitem-rtl {", -"padding-left: 5px;","padding-right: 28px;","}",".blocklyVerticalMarker {","stroke-width: 3px;","fill: rgba(255,255,255,.5);","pointer-events: none","}",".blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,",".blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon,",".blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox,",".blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {","background: url(<<>>/sprites.png) no-repeat -48px -16px;","}",".blocklyWidgetDiv .goog-menu {", -"background: #fff;","border-color: transparent;","border-style: solid;","border-width: 1px;","cursor: default;","font: normal 13px Arial, sans-serif;","margin: 0;","outline: none;","padding: 4px 0;","position: absolute;","overflow-y: auto;","overflow-x: hidden;","max-height: 100%;","z-index: 20000;","box-shadow: 0px 0px 3px 1px rgba(0,0,0,.3);","}",".blocklyWidgetDiv .goog-menu.focused {","box-shadow: 0px 0px 6px 1px rgba(0,0,0,.3);","}",".blocklyDropDownDiv .goog-menu {","cursor: default;",'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;', -"outline: none;","z-index: 20000;","}",".blocklyWidgetDiv .goog-menuitem,",".blocklyDropDownDiv .goog-menuitem {","color: #000;","font: normal 13px Arial, sans-serif;","list-style: none;","margin: 0;","min-width: 7em;","border: none;","padding: 6px 15px;","white-space: nowrap;","cursor: pointer;","}",".blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,",".blocklyWidgetDiv .goog-menu-noicon .goog-menuitem,",".blocklyDropDownDiv .goog-menu-nocheckbox .goog-menuitem,",".blocklyDropDownDiv .goog-menu-noicon .goog-menuitem {", -"padding-left: 12px;","}",".blocklyWidgetDiv .goog-menuitem-content,",".blocklyDropDownDiv .goog-menuitem-content {","font-family: Arial, sans-serif;","font-size: 13px;","}",".blocklyWidgetDiv .goog-menuitem-content {","color: #000;","}",".blocklyDropDownDiv .goog-menuitem-content {","color: #000;","}",".blocklyWidgetDiv .goog-menuitem-disabled,",".blocklyDropDownDiv .goog-menuitem-disabled {","cursor: inherit;","}",".blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content,",".blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-content {", -"color: #ccc !important;","}",".blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon,",".blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-icon {","opacity: .3;","filter: alpha(opacity=30);","}",".blocklyWidgetDiv .goog-menuitem-highlight ,",".blocklyDropDownDiv .goog-menuitem-highlight {","background-color: rgba(0,0,0,.1);","}",".blocklyWidgetDiv .goog-menuitem-checkbox,",".blocklyWidgetDiv .goog-menuitem-icon,",".blocklyDropDownDiv .goog-menuitem-checkbox,",".blocklyDropDownDiv .goog-menuitem-icon {", -"background-repeat: no-repeat;","height: 16px;","left: 6px;","position: absolute;","right: auto;","vertical-align: middle;","width: 16px;","}",".blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,",".blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon,",".blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox,",".blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {","left: auto;","right: 6px;","}",".blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,",".blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon,", -".blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox,",".blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {","position: static;","float: left;","margin-left: -24px;","}",".blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,",".blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon,",".blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox,",".blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {","float: right;","margin-right: -24px;","}",".blocklyComputeCanvas {", -"position: absolute;","width: 0;","height: 0;","}",".blocklyNoPointerEvents {","pointer-events: none;","}"];Blockly.utils.math={};Blockly.utils.math.toRadians=function(a){return a*Math.PI/180};Blockly.utils.math.toDegrees=function(a){return 180*a/Math.PI};Blockly.utils.math.clamp=function(a,b,c){if(c>>/sprites.png) no-repeat -48px -16px;","float: left;","margin-left: -24px;","position: static;","}",".blocklyMenuItemRtl .blocklyMenuItemCheckbox {","float: right;","margin-right: -24px;","}"];Blockly.utils.math={};Blockly.utils.math.toRadians=function(a){return a*Math.PI/180};Blockly.utils.math.toDegrees=function(a){return 180*a/Math.PI};Blockly.utils.math.clamp=function(a,b,c){if(ce.top?Blockly.DropDownDiv.getPositionAboveMetrics_(c,d,e,f):b+f.heightdocument.documentElement.clientTop?Blockly.DropDownDiv.getPositionAboveMetrics_(c, d,e,f):Blockly.DropDownDiv.getPositionTopOfPageMetrics_(a,e,f)};Blockly.DropDownDiv.getPositionBelowMetrics_=function(a,b,c,d){a=Blockly.DropDownDiv.getPositionX(a,c.left,c.right,d.width);return{initialX:a.divX,initialY:b,finalX:a.divX,finalY:b+Blockly.DropDownDiv.PADDING_Y,arrowX:a.arrowX,arrowY:-(Blockly.DropDownDiv.ARROW_SIZE/2+Blockly.DropDownDiv.BORDER_SIZE),arrowAtTop:!0,arrowVisible:!0}}; Blockly.DropDownDiv.getPositionAboveMetrics_=function(a,b,c,d){a=Blockly.DropDownDiv.getPositionX(a,c.left,c.right,d.width);return{initialX:a.divX,initialY:b-d.height,finalX:a.divX,finalY:b-d.height-Blockly.DropDownDiv.PADDING_Y,arrowX:a.arrowX,arrowY:d.height-2*Blockly.DropDownDiv.BORDER_SIZE-Blockly.DropDownDiv.ARROW_SIZE/2,arrowAtTop:!1,arrowVisible:!0}}; -Blockly.DropDownDiv.getPositionTopOfPageMetrics_=function(a,b,c){a=Blockly.DropDownDiv.getPositionX(a,b.left,b.right,c.width);return{initialX:a.divX,initialY:0,finalX:a.divX,finalY:0,arrowVisible:!1}};Blockly.DropDownDiv.getPositionX=function(a,b,c,d){var e=a;a=Blockly.utils.math.clamp(b,a-d/2,c-d);e-=Blockly.DropDownDiv.ARROW_SIZE/2;b=Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING;d=Blockly.utils.math.clamp(b,e-a,d-b-Blockly.DropDownDiv.ARROW_SIZE);return{arrowX:d,divX:a}}; -Blockly.DropDownDiv.isVisible=function(){return!!Blockly.DropDownDiv.owner_};Blockly.DropDownDiv.hideIfOwner=function(a,b){return Blockly.DropDownDiv.owner_===a?(b?Blockly.DropDownDiv.hideWithoutAnimation():Blockly.DropDownDiv.hide(),!0):!1}; +Blockly.DropDownDiv.getPositionTopOfPageMetrics_=function(a,b,c){a=Blockly.DropDownDiv.getPositionX(a,b.left,b.right,c.width);return{initialX:a.divX,initialY:0,finalX:a.divX,finalY:0,arrowAtTop:null,arrowX:null,arrowY:null,arrowVisible:!1}}; +Blockly.DropDownDiv.getPositionX=function(a,b,c,d){var e=a;a=Blockly.utils.math.clamp(b,a-d/2,c-d);e-=Blockly.DropDownDiv.ARROW_SIZE/2;b=Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING;d=Blockly.utils.math.clamp(b,e-a,d-b-Blockly.DropDownDiv.ARROW_SIZE);return{arrowX:d,divX:a}};Blockly.DropDownDiv.isVisible=function(){return!!Blockly.DropDownDiv.owner_}; +Blockly.DropDownDiv.hideIfOwner=function(a,b){return Blockly.DropDownDiv.owner_===a?(b?Blockly.DropDownDiv.hideWithoutAnimation():Blockly.DropDownDiv.hide(),!0):!1}; Blockly.DropDownDiv.hide=function(){var a=Blockly.DropDownDiv.DIV_;a.style.transform="translate(0, 0)";a.style.opacity=0;Blockly.DropDownDiv.animateOutTimer_=setTimeout(function(){Blockly.DropDownDiv.hideWithoutAnimation()},1E3*Blockly.DropDownDiv.ANIMATION_TIME);Blockly.DropDownDiv.onHide_&&(Blockly.DropDownDiv.onHide_(),Blockly.DropDownDiv.onHide_=null)}; Blockly.DropDownDiv.hideWithoutAnimation=function(){if(Blockly.DropDownDiv.isVisible()){Blockly.DropDownDiv.animateOutTimer_&&clearTimeout(Blockly.DropDownDiv.animateOutTimer_);var a=Blockly.DropDownDiv.DIV_;a.style.transform="";a.style.left="";a.style.top="";a.style.opacity=0;a.style.display="none";a.style.backgroundColor="";a.style.borderColor="";Blockly.DropDownDiv.onHide_&&(Blockly.DropDownDiv.onHide_(),Blockly.DropDownDiv.onHide_=null);Blockly.DropDownDiv.clearContent();Blockly.DropDownDiv.owner_= null;Blockly.DropDownDiv.rendererClassName_&&(Blockly.utils.dom.removeClass(a,Blockly.DropDownDiv.rendererClassName_),Blockly.DropDownDiv.rendererClassName_="");Blockly.DropDownDiv.themeClassName_&&(Blockly.utils.dom.removeClass(a,Blockly.DropDownDiv.themeClassName_),Blockly.DropDownDiv.themeClassName_="");Blockly.getMainWorkspace().markFocused()}}; Blockly.DropDownDiv.positionInternal_=function(a,b,c,d){a=Blockly.DropDownDiv.getPositionMetrics_(a,b,c,d);a.arrowVisible?(Blockly.DropDownDiv.arrow_.style.display="",Blockly.DropDownDiv.arrow_.style.transform="translate("+a.arrowX+"px,"+a.arrowY+"px) rotate(45deg)",Blockly.DropDownDiv.arrow_.setAttribute("class",a.arrowAtTop?"blocklyDropDownArrow blocklyArrowTop":"blocklyDropDownArrow blocklyArrowBottom")):Blockly.DropDownDiv.arrow_.style.display="none";b=Math.floor(a.initialX);c=Math.floor(a.initialY); -d=Math.floor(a.finalX);var e=Math.floor(a.finalY),f=Blockly.DropDownDiv.DIV_;f.style.left=b+"px";f.style.top=c+"px";f.style.display="block";f.style.opacity=1;f.style.transform="translate("+(d-b)+"px,"+(e-c)+"px)";return a.arrowAtTop}; -Blockly.DropDownDiv.repositionForWindowResize=function(){if(Blockly.DropDownDiv.owner_){var a=Blockly.DropDownDiv.owner_,b=Blockly.DropDownDiv.owner_.getSourceBlock();a=Blockly.DropDownDiv.positionToField_?Blockly.DropDownDiv.getScaledBboxOfField_(a):Blockly.DropDownDiv.getScaledBboxOfBlock_(b);b=a.left+(a.right-a.left)/2;Blockly.DropDownDiv.positionInternal_(b,a.bottom,b,a.top)}else Blockly.DropDownDiv.hide()};Blockly.Grid=function(a,b){this.gridPattern_=a;this.spacing_=b.spacing;this.length_=b.length;this.line2_=(this.line1_=a.firstChild)&&this.line1_.nextSibling;this.snapToGrid_=b.snap};Blockly.Grid.prototype.scale_=1;Blockly.Grid.prototype.dispose=function(){this.gridPattern_=null};Blockly.Grid.prototype.shouldSnap=function(){return this.snapToGrid_};Blockly.Grid.prototype.getSpacing=function(){return this.spacing_};Blockly.Grid.prototype.getPatternId=function(){return this.gridPattern_.id}; +d=Math.floor(a.finalX);var e=Math.floor(a.finalY),f=Blockly.DropDownDiv.DIV_;f.style.left=b+"px";f.style.top=c+"px";f.style.display="block";f.style.opacity=1;f.style.transform="translate("+(d-b)+"px,"+(e-c)+"px)";return!!a.arrowAtTop}; +Blockly.DropDownDiv.repositionForWindowResize=function(){if(Blockly.DropDownDiv.owner_){var a=Blockly.DropDownDiv.owner_,b=a.getSourceBlock();a=Blockly.DropDownDiv.positionToField_?Blockly.DropDownDiv.getScaledBboxOfField_(a):Blockly.DropDownDiv.getScaledBboxOfBlock_(b);b=a.left+(a.right-a.left)/2;Blockly.DropDownDiv.positionInternal_(b,a.bottom,b,a.top)}else Blockly.DropDownDiv.hide()};Blockly.Grid=function(a,b){this.gridPattern_=a;this.spacing_=b.spacing;this.length_=b.length;this.line2_=(this.line1_=a.firstChild)&&this.line1_.nextSibling;this.snapToGrid_=b.snap};Blockly.Grid.prototype.scale_=1;Blockly.Grid.prototype.dispose=function(){this.gridPattern_=null};Blockly.Grid.prototype.shouldSnap=function(){return this.snapToGrid_};Blockly.Grid.prototype.getSpacing=function(){return this.spacing_};Blockly.Grid.prototype.getPatternId=function(){return this.gridPattern_.id}; Blockly.Grid.prototype.update=function(a){this.scale_=a;var b=this.spacing_*a||100;this.gridPattern_.setAttribute("width",b);this.gridPattern_.setAttribute("height",b);b=Math.floor(this.spacing_/2)+.5;var c=b-this.length_/2,d=b+this.length_/2;b*=a;c*=a;d*=a;this.setLineAttributes_(this.line1_,a,c,d,b,b);this.setLineAttributes_(this.line2_,a,b,b,c,d)}; Blockly.Grid.prototype.setLineAttributes_=function(a,b,c,d,e,f){a&&(a.setAttribute("stroke-width",b),a.setAttribute("x1",c),a.setAttribute("y1",e),a.setAttribute("x2",d),a.setAttribute("y2",f))};Blockly.Grid.prototype.moveTo=function(a,b){this.gridPattern_.setAttribute("x",a);this.gridPattern_.setAttribute("y",b);(Blockly.utils.userAgent.IE||Blockly.utils.userAgent.EDGE)&&this.update(this.scale_)}; -Blockly.Grid.createDom=function(a,b,c){a=Blockly.utils.dom.createSvgElement("pattern",{id:"blocklyGridPattern"+a,patternUnits:"userSpaceOnUse"},c);0b.indexOf(d))throw Error(d+" is not a valid modifier key.");};Blockly.user.keyMap.createSerializedKey=function(a,b){var c="",d=Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys);Blockly.user.keyMap.checkModifiers_(b,d);for(var e=0,f;f=d[e];e++)-1]*[^/])?>[^<]*)\n([^<]*<\/)/;do{var c=a;a=a.replace(b,"$1 $2")}while(a!=c);return a.replace(/<(\w+)([^<]*)\/>/g,"<$1$2>")}; Blockly.Xml.domToPrettyText=function(a){a=Blockly.Xml.domToText(a).split("<");for(var b="",c=1;c"!=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){var b=Blockly.utils.xml.textToDomDocument(a);if(!b||!b.documentElement||b.getElementsByTagName("parsererror").length)throw Error("textToDom was unable to parse: "+a);return b.documentElement};Blockly.Xml.clearWorkspaceAndLoadFromXml=function(a,b){b.setResizesEnabled(!1);b.clear();a=Blockly.Xml.domToWorkspace(a,b);b.setResizesEnabled(!0);return a}; @@ -163,41 +176,41 @@ Blockly.WorkspaceComment.fromXml(l,b):console.warn("Missing require for Blockly. return c};Blockly.Xml.appendDomToWorkspace=function(a,b){var c;b.hasOwnProperty("scale")&&(c=b.getBlocksBoundingBox());a=Blockly.Xml.domToWorkspace(a,b);if(c&&c.top!=c.bottom){var d=c.bottom;var e=b.RTL?c.right:c.left;var f=Infinity,g=-Infinity,h=Infinity;for(c=0;cg&&(g=k.x)}d=d-h+10;e=b.RTL?e-g:e-f;for(c=0;c document.");}else a=null;return a};Blockly.Touch={};Blockly.Touch.TOUCH_ENABLED="ontouchstart"in Blockly.utils.global||!!(Blockly.utils.global.document&&document.documentElement&&"ontouchstart"in document.documentElement)||!(!Blockly.utils.global.navigator||!Blockly.utils.global.navigator.maxTouchPoints&&!Blockly.utils.global.navigator.msMaxTouchPoints);Blockly.Touch.touchIdentifier_=null;Blockly.Touch.TOUCH_MAP={}; Blockly.utils.global.PointerEvent?Blockly.Touch.TOUCH_MAP={mousedown:["pointerdown"],mouseenter:["pointerenter"],mouseleave:["pointerleave"],mousemove:["pointermove"],mouseout:["pointerout"],mouseover:["pointerover"],mouseup:["pointerup","pointercancel"],touchend:["pointerup"],touchcancel:["pointercancel"]}:Blockly.Touch.TOUCH_ENABLED&&(Blockly.Touch.TOUCH_MAP={mousedown:["touchstart"],mousemove:["touchmove"],mouseup:["touchend","touchcancel"]});Blockly.longPid_=0; Blockly.longStart=function(a,b){Blockly.longStop_();a.changedTouches&&1!=a.changedTouches.length||(Blockly.longPid_=setTimeout(function(){a.changedTouches&&(a.button=2,a.clientX=a.changedTouches[0].clientX,a.clientY=a.changedTouches[0].clientY);b&&b.handleRightClick(a)},Blockly.LONGPRESS))};Blockly.longStop_=function(){Blockly.longPid_&&(clearTimeout(Blockly.longPid_),Blockly.longPid_=0)};Blockly.Touch.clearTouchIdentifier=function(){Blockly.Touch.touchIdentifier_=null}; Blockly.Touch.shouldHandleEvent=function(a){return!Blockly.Touch.isMouseOrTouchEvent(a)||Blockly.Touch.checkTouchIdentifier(a)};Blockly.Touch.getTouchIdentifierFromEvent=function(a){return void 0!=a.pointerId?a.pointerId:a.changedTouches&&a.changedTouches[0]&&void 0!==a.changedTouches[0].identifier&&null!==a.changedTouches[0].identifier?a.changedTouches[0].identifier:"mouse"}; Blockly.Touch.checkTouchIdentifier=function(a){var b=Blockly.Touch.getTouchIdentifierFromEvent(a);return void 0!==Blockly.Touch.touchIdentifier_&&null!==Blockly.Touch.touchIdentifier_?Blockly.Touch.touchIdentifier_==b:"mousedown"==a.type||"touchstart"==a.type||"pointerdown"==a.type?(Blockly.Touch.touchIdentifier_=b,!0):!1};Blockly.Touch.setClientFromTouch=function(a){if(Blockly.utils.string.startsWith(a.type,"touch")){var b=a.changedTouches[0];a.clientX=b.clientX;a.clientY=b.clientY}}; Blockly.Touch.isMouseOrTouchEvent=function(a){return Blockly.utils.string.startsWith(a.type,"touch")||Blockly.utils.string.startsWith(a.type,"mouse")||Blockly.utils.string.startsWith(a.type,"pointer")};Blockly.Touch.isTouchEvent=function(a){return Blockly.utils.string.startsWith(a.type,"touch")||Blockly.utils.string.startsWith(a.type,"pointer")}; -Blockly.Touch.splitEventByTouches=function(a){var b=[];if(a.changedTouches)for(var c=0;c=a||isNaN(a)||this.scrollViewSize_=a||isNaN(a)||this.scrollViewSize_c+window.scrollY&&(e-=Blockly.Tooltip.DIV.offsetHeight+2*Blockly.Tooltip.OFFSET_Y);a?d=Math.max(Blockly.Tooltip.MARGINS-window.scrollX, -d):d+Blockly.Tooltip.DIV.offsetWidth>b+window.scrollX-2*Blockly.Tooltip.MARGINS&&(d=b-Blockly.Tooltip.DIV.offsetWidth-2*Blockly.Tooltip.MARGINS);Blockly.Tooltip.DIV.style.top=e+"px";Blockly.Tooltip.DIV.style.left=d+"px"}};Blockly.WorkspaceDragSurfaceSvg=function(a){this.container_=a;this.createDom()};Blockly.WorkspaceDragSurfaceSvg.prototype.SVG_=null;Blockly.WorkspaceDragSurfaceSvg.prototype.dragGroup_=null;Blockly.WorkspaceDragSurfaceSvg.prototype.container_=null; +d):d+Blockly.Tooltip.DIV.offsetWidth>b+window.scrollX-2*Blockly.Tooltip.MARGINS&&(d=b-Blockly.Tooltip.DIV.offsetWidth-2*Blockly.Tooltip.MARGINS);Blockly.Tooltip.DIV.style.top=e+"px";Blockly.Tooltip.DIV.style.left=d+"px"}};Blockly.WorkspaceDragSurfaceSvg=function(a){this.container_=a;this.createDom()};Blockly.WorkspaceDragSurfaceSvg.prototype.SVG_=null;Blockly.WorkspaceDragSurfaceSvg.prototype.container_=null; Blockly.WorkspaceDragSurfaceSvg.prototype.createDom=function(){this.SVG_||(this.SVG_=Blockly.utils.dom.createSvgElement("svg",{xmlns:Blockly.utils.dom.SVG_NS,"xmlns:html":Blockly.utils.dom.HTML_NS,"xmlns:xlink":Blockly.utils.dom.XLINK_NS,version:"1.1","class":"blocklyWsDragSurface blocklyOverflowVisible"},null),this.container_.appendChild(this.SVG_))}; Blockly.WorkspaceDragSurfaceSvg.prototype.translateSurface=function(a,b){a=a.toFixed(0);b=b.toFixed(0);this.SVG_.style.display="block";Blockly.utils.dom.setCssTransform(this.SVG_,"translate3d("+a+"px, "+b+"px, 0px)")};Blockly.WorkspaceDragSurfaceSvg.prototype.getSurfaceTranslation=function(){return Blockly.utils.getRelativeXY(this.SVG_)}; Blockly.WorkspaceDragSurfaceSvg.prototype.clearAndHide=function(a){if(!a)throw Error("Couldn't clear and hide the drag surface: missing new surface.");var b=this.SVG_.childNodes[0],c=this.SVG_.childNodes[1];if(!(b&&c&&Blockly.utils.dom.hasClass(b,"blocklyBlockCanvas")&&Blockly.utils.dom.hasClass(c,"blocklyBubbleCanvas")))throw Error("Couldn't clear and hide the drag surface. A node was missing.");null!=this.previousSibling_?Blockly.utils.dom.insertAfter(b,this.previousSibling_):a.insertBefore(b,a.firstChild); Blockly.utils.dom.insertAfter(c,b);this.SVG_.style.display="none";if(this.SVG_.childNodes.length)throw Error("Drag surface was not cleared.");Blockly.utils.dom.setCssTransform(this.SVG_,"");this.previousSibling_=null}; -Blockly.WorkspaceDragSurfaceSvg.prototype.setContentsAndShow=function(a,b,c,d,e,f){if(this.SVG_.childNodes.length)throw Error("Already dragging a block.");this.previousSibling_=c;a.setAttribute("transform","translate(0, 0) scale("+f+")");b.setAttribute("transform","translate(0, 0) scale("+f+")");this.SVG_.setAttribute("width",d);this.SVG_.setAttribute("height",e);this.SVG_.appendChild(a);this.SVG_.appendChild(b);this.SVG_.style.display="block"};Blockly.ASTNode=function(a,b,c){if(!b)throw Error("Cannot create a node without a location.");this.type_=a;this.isConnection_=Blockly.ASTNode.isConnectionType_(a);this.location_=b;this.processParams_(c||null)};Blockly.ASTNode.types={FIELD:"field",BLOCK:"block",INPUT:"input",OUTPUT:"output",NEXT:"next",PREVIOUS:"previous",STACK:"stack",WORKSPACE:"workspace"};Blockly.ASTNode.NAVIGATE_ALL_FIELDS=!1;Blockly.ASTNode.DEFAULT_OFFSET_Y=-20;Blockly.ASTNode.isConnectionType_=function(a){switch(a){case Blockly.ASTNode.types.PREVIOUS:case Blockly.ASTNode.types.NEXT:case Blockly.ASTNode.types.INPUT:case Blockly.ASTNode.types.OUTPUT:return!0}return!1}; +Blockly.WorkspaceDragSurfaceSvg.prototype.setContentsAndShow=function(a,b,c,d,e,f){if(this.SVG_.childNodes.length)throw Error("Already dragging a block.");this.previousSibling_=c;a.setAttribute("transform","translate(0, 0) scale("+f+")");b.setAttribute("transform","translate(0, 0) scale("+f+")");this.SVG_.setAttribute("width",d);this.SVG_.setAttribute("height",e);this.SVG_.appendChild(a);this.SVG_.appendChild(b);this.SVG_.style.display="block"};Blockly.ASTNode=function(a,b,c){if(!b)throw Error("Cannot create a node without a location.");this.type_=a;this.isConnection_=Blockly.ASTNode.isConnectionType_(a);this.location_=b;this.wsCoordinate_=null;this.processParams_(c||null)};Blockly.ASTNode.types={FIELD:"field",BLOCK:"block",INPUT:"input",OUTPUT:"output",NEXT:"next",PREVIOUS:"previous",STACK:"stack",WORKSPACE:"workspace"};Blockly.ASTNode.NAVIGATE_ALL_FIELDS=!1;Blockly.ASTNode.DEFAULT_OFFSET_Y=-20;Blockly.ASTNode.isConnectionType_=function(a){switch(a){case Blockly.ASTNode.types.PREVIOUS:case Blockly.ASTNode.types.NEXT:case Blockly.ASTNode.types.INPUT:case Blockly.ASTNode.types.OUTPUT:return!0}return!1}; Blockly.ASTNode.createFieldNode=function(a){return a?new Blockly.ASTNode(Blockly.ASTNode.types.FIELD,a):null}; Blockly.ASTNode.createConnectionNode=function(a){return a?a.type==Blockly.INPUT_VALUE||a.type==Blockly.NEXT_STATEMENT&&a.getParentInput()?Blockly.ASTNode.createInputNode(a.getParentInput()):a.type==Blockly.NEXT_STATEMENT?new Blockly.ASTNode(Blockly.ASTNode.types.NEXT,a):a.type==Blockly.OUTPUT_VALUE?new Blockly.ASTNode(Blockly.ASTNode.types.OUTPUT,a):a.type==Blockly.PREVIOUS_STATEMENT?new Blockly.ASTNode(Blockly.ASTNode.types.PREVIOUS,a):null:null}; Blockly.ASTNode.createInputNode=function(a){return a&&a.connection?new Blockly.ASTNode(Blockly.ASTNode.types.INPUT,a.connection):null};Blockly.ASTNode.createBlockNode=function(a){return a?new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK,a):null};Blockly.ASTNode.createStackNode=function(a){return a?new Blockly.ASTNode(Blockly.ASTNode.types.STACK,a):null};Blockly.ASTNode.createWorkspaceNode=function(a,b){return b&&a?new Blockly.ASTNode(Blockly.ASTNode.types.WORKSPACE,a,{wsCoordinate:b}):null}; -Blockly.ASTNode.prototype.processParams_=function(a){a&&a.wsCoordinate&&(this.wsCoordinate_=a.wsCoordinate)};Blockly.ASTNode.prototype.getLocation=function(){return this.location_};Blockly.ASTNode.prototype.getType=function(){return this.type_};Blockly.ASTNode.prototype.getWsCoordinate=function(){return this.wsCoordinate_};Blockly.ASTNode.prototype.isConnection=function(){return this.isConnection_}; -Blockly.ASTNode.prototype.findNextForInput_=function(){var a=this.location_.getParentInput(),b=a.getSourceBlock();a=b.inputList.indexOf(a)+1;for(var c;c=b.inputList[a];a++){for(var d=c.fieldRow,e=0,f;f=d[e];e++)if(f.isClickable()||Blockly.ASTNode.NAVIGATE_ALL_FIELDS)return Blockly.ASTNode.createFieldNode(f);if(c.connection)return Blockly.ASTNode.createInputNode(c)}return null}; +Blockly.ASTNode.createTopNode=function(a){var b=a.previousConnection||a.outputConnection;return b?Blockly.ASTNode.createConnectionNode(b):Blockly.ASTNode.createBlockNode(a)};Blockly.ASTNode.prototype.processParams_=function(a){a&&a.wsCoordinate&&(this.wsCoordinate_=a.wsCoordinate)};Blockly.ASTNode.prototype.getLocation=function(){return this.location_};Blockly.ASTNode.prototype.getType=function(){return this.type_};Blockly.ASTNode.prototype.getWsCoordinate=function(){return this.wsCoordinate_}; +Blockly.ASTNode.prototype.isConnection=function(){return this.isConnection_};Blockly.ASTNode.prototype.findNextForInput_=function(){var a=this.location_.getParentInput(),b=a.getSourceBlock();a=b.inputList.indexOf(a)+1;for(var c;c=b.inputList[a];a++){for(var d=c.fieldRow,e=0,f;f=d[e];e++)if(f.isClickable()||Blockly.ASTNode.NAVIGATE_ALL_FIELDS)return Blockly.ASTNode.createFieldNode(f);if(c.connection)return Blockly.ASTNode.createInputNode(c)}return null}; Blockly.ASTNode.prototype.findNextForField_=function(){var a=this.location_,b=a.getParentInput(),c=a.getSourceBlock(),d=c.inputList.indexOf(b);for(a=b.fieldRow.indexOf(a)+1;b=c.inputList[d];d++){for(var e=b.fieldRow;ac)){var d=b.getSvgXY(a.getSvgRoot());a.outputConnection?(d.x+=(a.RTL?3:-3)*c,d.y+=13*c):a.previousConnection&&(d.x+=(a.RTL?-23:23)*c,d.y+=3*c);a=Blockly.utils.dom.createSvgElement("circle",{cx:d.x,cy:d.y,r:0,fill:"none",stroke:"#888","stroke-width":10},b.getParentSvg());Blockly.blockAnimations.connectionUiStep_(a,new Date,c)}}; Blockly.blockAnimations.connectionUiStep_=function(a,b,c){var d=(new Date-b)/150;1a.workspace.scale)){var b=a.getHeightWidth().height;b=Math.atan(10/b)/Math.PI*180;a.RTL||(b*=-1);Blockly.blockAnimations.disconnectUiStep_(a.getSvgRoot(),b,new Date)}}; Blockly.blockAnimations.disconnectUiStep_=function(a,b,c){var d=(new Date-c)/200;1b-Blockly.CURRENT_CONNECTION_PREFERENCE)}if(this.localConnection_||this.closestConnection_)console.error("Only one of localConnection_ and closestConnection_ was set."); else return!0}else return!(!this.localConnection_||!this.closestConnection_);console.error("Returning true from shouldUpdatePreviews, but it's not clear why.");return!0};Blockly.InsertionMarkerManager.prototype.getCandidate_=function(a){for(var b=this.getStartRadius_(),c=null,d=null,e=0;ethis.remainingCapacityOfType(c))return!1;b+=a[c]}return b>this.remainingCapacity()?!1:!0};Blockly.Workspace.prototype.hasBlockLimits=function(){return Infinity!=this.options.maxBlocks||!!this.options.maxInstances}; -Blockly.Workspace.prototype.undo=function(a){var b=a?this.redoStack_:this.undoStack_,c=a?this.undoStack_:this.redoStack_,d=b.pop();if(d){for(var e=[d];b.length&&d.group&&d.group==b[b.length-1].group;)e.push(b.pop());for(b=0;d=e[b];b++)c.push(d);e=Blockly.Events.filter(e,a);Blockly.Events.recordUndo=!1;try{for(b=0;d=e[b];b++)d.run(a)}finally{Blockly.Events.recordUndo=!0}}};Blockly.Workspace.prototype.clearUndo=function(){this.undoStack_.length=0;this.redoStack_.length=0;Blockly.Events.clearPendingUndo()}; -Blockly.Workspace.prototype.addChangeListener=function(a){this.listeners_.push(a);return a};Blockly.Workspace.prototype.removeChangeListener=function(a){Blockly.utils.arrayRemove(this.listeners_,a)};Blockly.Workspace.prototype.fireChangeListener=function(a){if(a.recordUndo)for(this.undoStack_.push(a),this.redoStack_.length=0;this.undoStack_.length>this.MAX_UNDO&&0<=this.MAX_UNDO;)this.undoStack_.shift();for(var b=0,c;c=this.listeners_[b];b++)c(a)}; -Blockly.Workspace.prototype.getBlockById=function(a){return this.blockDB_[a]||null};Blockly.Workspace.prototype.setBlockById=function(a,b){this.blockDB_[a]=b};Blockly.Workspace.prototype.removeBlockById=function(a){delete this.blockDB_[a]};Blockly.Workspace.prototype.getCommentById=function(a){return this.commentDB_[a]||null};Blockly.Workspace.prototype.allInputsFilled=function(a){for(var b=this.getTopBlocks(!1),c=0,d;d=b[c];c++)if(!d.allInputsFilled(a))return!1;return!0}; -Blockly.Workspace.prototype.getPotentialVariableMap=function(){return this.potentialVariableMap_};Blockly.Workspace.prototype.createPotentialVariableMap=function(){this.potentialVariableMap_=new Blockly.VariableMap(this)};Blockly.Workspace.prototype.getVariableMap=function(){return this.variableMap_};Blockly.Workspace.prototype.setVariableMap=function(a){this.variableMap_=a};Blockly.Workspace.WorkspaceDB_=Object.create(null); -Blockly.Workspace.getById=function(a){return Blockly.Workspace.WorkspaceDB_[a]||null};Blockly.Workspace.getAll=function(){var a=[],b;for(b in Blockly.Workspace.WorkspaceDB_)a.push(Blockly.Workspace.WorkspaceDB_[b]);return a};Blockly.Bubble=function(a,b,c,d,e,f){this.workspace_=a;this.content_=b;this.shape_=c;this.onMouseDownResizeWrapper_=this.onMouseDownBubbleWrapper_=this.moveCallback_=this.resizeCallback_=null;this.disposed=!1;c=Blockly.Bubble.ARROW_ANGLE;this.workspace_.RTL&&(c=-c);this.arrow_radians_=Blockly.utils.math.toRadians(c);a.getBubbleCanvas().appendChild(this.createDom_(b,!(!e||!f)));this.setAnchorLocation(d);e&&f||(a=this.content_.getBBox(),e=a.width+2*Blockly.Bubble.BORDER_WIDTH,f=a.height+2*Blockly.Bubble.BORDER_WIDTH); +Blockly.Workspace.prototype.variableIndexOf=function(a){console.warn("Deprecated call to Blockly.Workspace.prototype.variableIndexOf");return-1};Blockly.Workspace.prototype.getVariable=function(a,b){return this.variableMap_.getVariable(a,b)};Blockly.Workspace.prototype.getVariableById=function(a){return this.variableMap_.getVariableById(a)};Blockly.Workspace.prototype.getVariablesOfType=function(a){return this.variableMap_.getVariablesOfType(a)};Blockly.Workspace.prototype.getVariableTypes=function(){return this.variableMap_.getVariableTypes(this)}; +Blockly.Workspace.prototype.getAllVariables=function(){return this.variableMap_.getAllVariables()};Blockly.Workspace.prototype.getAllVariableNames=function(){return this.variableMap_.getAllVariableNames()};Blockly.Workspace.prototype.getWidth=function(){return 0};Blockly.Workspace.prototype.newBlock=function(a,b){return new Blockly.Block(this,a,b)};Blockly.Workspace.prototype.remainingCapacity=function(){return isNaN(this.options.maxBlocks)?Infinity:this.options.maxBlocks-this.getAllBlocks(!1).length}; +Blockly.Workspace.prototype.remainingCapacityOfType=function(a){return this.options.maxInstances?(void 0!==this.options.maxInstances[a]?this.options.maxInstances[a]:Infinity)-this.getBlocksByType(a,!1).length:Infinity};Blockly.Workspace.prototype.isCapacityAvailable=function(a){if(!this.hasBlockLimits())return!0;var b=0,c;for(c in a){if(a[c]>this.remainingCapacityOfType(c))return!1;b+=a[c]}return b>this.remainingCapacity()?!1:!0}; +Blockly.Workspace.prototype.hasBlockLimits=function(){return Infinity!=this.options.maxBlocks||!!this.options.maxInstances};Blockly.Workspace.prototype.undo=function(a){var b=a?this.redoStack_:this.undoStack_,c=a?this.undoStack_:this.redoStack_,d=b.pop();if(d){for(var e=[d];b.length&&d.group&&d.group==b[b.length-1].group;)e.push(b.pop());for(b=0;d=e[b];b++)c.push(d);e=Blockly.Events.filter(e,a);Blockly.Events.recordUndo=!1;try{for(b=0;d=e[b];b++)d.run(a)}finally{Blockly.Events.recordUndo=!0}}}; +Blockly.Workspace.prototype.clearUndo=function(){this.undoStack_.length=0;this.redoStack_.length=0;Blockly.Events.clearPendingUndo()};Blockly.Workspace.prototype.addChangeListener=function(a){this.listeners_.push(a);return a};Blockly.Workspace.prototype.removeChangeListener=function(a){Blockly.utils.arrayRemove(this.listeners_,a)}; +Blockly.Workspace.prototype.fireChangeListener=function(a){if(a.recordUndo)for(this.undoStack_.push(a),this.redoStack_.length=0;this.undoStack_.length>this.MAX_UNDO&&0<=this.MAX_UNDO;)this.undoStack_.shift();for(var b=0,c;c=this.listeners_[b];b++)c(a)};Blockly.Workspace.prototype.getBlockById=function(a){return this.blockDB_[a]||null};Blockly.Workspace.prototype.setBlockById=function(a,b){this.blockDB_[a]=b};Blockly.Workspace.prototype.removeBlockById=function(a){delete this.blockDB_[a]}; +Blockly.Workspace.prototype.getCommentById=function(a){return this.commentDB_[a]||null};Blockly.Workspace.prototype.allInputsFilled=function(a){for(var b=this.getTopBlocks(!1),c=0,d;d=b[c];c++)if(!d.allInputsFilled(a))return!1;return!0};Blockly.Workspace.prototype.getPotentialVariableMap=function(){return this.potentialVariableMap_};Blockly.Workspace.prototype.createPotentialVariableMap=function(){this.potentialVariableMap_=new Blockly.VariableMap(this)}; +Blockly.Workspace.prototype.getVariableMap=function(){return this.variableMap_};Blockly.Workspace.prototype.setVariableMap=function(a){this.variableMap_=a};Blockly.Workspace.WorkspaceDB_=Object.create(null);Blockly.Workspace.getById=function(a){return Blockly.Workspace.WorkspaceDB_[a]||null};Blockly.Workspace.getAll=function(){var a=[],b;for(b in Blockly.Workspace.WorkspaceDB_)a.push(Blockly.Workspace.WorkspaceDB_[b]);return a};Blockly.Bubble=function(a,b,c,d,e,f){this.workspace_=a;this.content_=b;this.shape_=c;this.onMouseDownResizeWrapper_=this.onMouseDownBubbleWrapper_=this.moveCallback_=this.resizeCallback_=null;this.disposed=!1;c=Blockly.Bubble.ARROW_ANGLE;this.workspace_.RTL&&(c=-c);this.arrow_radians_=Blockly.utils.math.toRadians(c);a.getBubbleCanvas().appendChild(this.createDom_(b,!(!e||!f)));this.setAnchorLocation(d);e&&f||(a=this.content_.getBBox(),e=a.width+2*Blockly.Bubble.BORDER_WIDTH,f=a.height+2*Blockly.Bubble.BORDER_WIDTH); this.setBubbleSize(e,f);this.positionBubble_();this.renderArrow_();this.rendered_=!0};Blockly.Bubble.BORDER_WIDTH=6;Blockly.Bubble.ARROW_THICKNESS=5;Blockly.Bubble.ARROW_ANGLE=20;Blockly.Bubble.ARROW_BEND=4;Blockly.Bubble.ANCHOR_RADIUS=8;Blockly.Bubble.onMouseUpWrapper_=null;Blockly.Bubble.onMouseMoveWrapper_=null; Blockly.Bubble.unbindDragEvents_=function(){Blockly.Bubble.onMouseUpWrapper_&&(Blockly.unbindEvent_(Blockly.Bubble.onMouseUpWrapper_),Blockly.Bubble.onMouseUpWrapper_=null);Blockly.Bubble.onMouseMoveWrapper_&&(Blockly.unbindEvent_(Blockly.Bubble.onMouseMoveWrapper_),Blockly.Bubble.onMouseMoveWrapper_=null)};Blockly.Bubble.bubbleMouseUp_=function(a){Blockly.Touch.clearTouchIdentifier();Blockly.Bubble.unbindDragEvents_()};Blockly.Bubble.prototype.rendered_=!1;Blockly.Bubble.prototype.anchorXY_=null; Blockly.Bubble.prototype.relativeLeft_=0;Blockly.Bubble.prototype.relativeTop_=0;Blockly.Bubble.prototype.width_=0;Blockly.Bubble.prototype.height_=0;Blockly.Bubble.prototype.autoLayout_=!0; @@ -374,45 +388,46 @@ Blockly.Events.CommentDelete.prototype.fromJson=function(a){Blockly.Events.Comme Blockly.Events.CommentMove.prototype.recordNew=function(){if(!this.comment_)throw Error("Tried to record the new position of a comment on the same event twice.");this.newCoordinate_=this.comment_.getXY();this.comment_=null};Blockly.Events.CommentMove.prototype.type=Blockly.Events.COMMENT_MOVE;Blockly.Events.CommentMove.prototype.setOldCoordinate=function(a){this.oldCoordinate_=a}; Blockly.Events.CommentMove.prototype.toJson=function(){var a=Blockly.Events.CommentMove.superClass_.toJson.call(this);this.newCoordinate_&&(a.newCoordinate=Math.round(this.newCoordinate_.x)+","+Math.round(this.newCoordinate_.y));return a};Blockly.Events.CommentMove.prototype.fromJson=function(a){Blockly.Events.CommentMove.superClass_.fromJson.call(this,a);a.newCoordinate&&(a=a.newCoordinate.split(","),this.newCoordinate_=new Blockly.utils.Coordinate(Number(a[0]),Number(a[1])))}; Blockly.Events.CommentMove.prototype.isNull=function(){return Blockly.utils.Coordinate.equals(this.oldCoordinate_,this.newCoordinate_)};Blockly.Events.CommentMove.prototype.run=function(a){var b=this.getEventWorkspace_().getCommentById(this.commentId);if(b){a=a?this.newCoordinate_:this.oldCoordinate_;var c=b.getXY();b.moveBy(a.x-c.x,a.y-c.y)}else console.warn("Can't move non-existent comment: "+this.commentId)};Blockly.BubbleDragger=function(a,b){this.draggingBubble_=a;this.workspace_=b;this.deleteArea_=null;this.wouldDeleteBubble_=!1;this.startXY_=this.draggingBubble_.getRelativeToSurfaceXY();this.dragSurface_=Blockly.utils.is3dSupported()&&b.getBlockDragSurface()?b.getBlockDragSurface():null};Blockly.BubbleDragger.prototype.dispose=function(){this.dragSurface_=this.workspace_=this.draggingBubble_=null}; -Blockly.BubbleDragger.prototype.startBubbleDrag=function(){Blockly.Events.getGroup()||Blockly.Events.setGroup(!0);this.workspace_.setResizesEnabled(!1);this.draggingBubble_.setAutoLayout(!1);this.dragSurface_&&this.moveToDragSurface_();this.draggingBubble_.setDragging&&this.draggingBubble_.setDragging(!0);var a=this.workspace_.getToolbox();if(a){var b=this.draggingBubble_.isDeletable()?"blocklyToolboxDelete":"blocklyToolboxGrab";a.addStyle(b)}}; +Blockly.BubbleDragger.prototype.startBubbleDrag=function(){Blockly.Events.getGroup()||Blockly.Events.setGroup(!0);this.workspace_.setResizesEnabled(!1);this.draggingBubble_.setAutoLayout(!1);this.dragSurface_&&this.moveToDragSurface_();this.draggingBubble_.setDragging&&this.draggingBubble_.setDragging(!0);var a=this.workspace_.getToolbox();if(a&&"function"==typeof a.addStyle){var b=this.draggingBubble_.isDeletable()?"blocklyToolboxDelete":"blocklyToolboxGrab";a.addStyle(b)}}; Blockly.BubbleDragger.prototype.dragBubble=function(a,b){b=this.pixelsToWorkspaceUnits_(b);b=Blockly.utils.Coordinate.sum(this.startXY_,b);this.draggingBubble_.moveDuringDrag(this.dragSurface_,b);this.draggingBubble_.isDeletable()&&(this.deleteArea_=this.workspace_.isDeleteArea(a),this.updateCursorDuringBubbleDrag_())}; Blockly.BubbleDragger.prototype.maybeDeleteBubble_=function(){var a=this.workspace_.trashcan;this.wouldDeleteBubble_?(a&&setTimeout(a.close.bind(a),100),this.fireMoveEvent_(),this.draggingBubble_.dispose(!1,!0)):a&&a.close();return this.wouldDeleteBubble_}; Blockly.BubbleDragger.prototype.updateCursorDuringBubbleDrag_=function(){this.wouldDeleteBubble_=this.deleteArea_!=Blockly.DELETE_AREA_NONE;var a=this.workspace_.trashcan;this.wouldDeleteBubble_?(this.draggingBubble_.setDeleteStyle(!0),this.deleteArea_==Blockly.DELETE_AREA_TRASH&&a&&a.setOpen(!0)):(this.draggingBubble_.setDeleteStyle(!1),a&&a.setOpen(!1))}; -Blockly.BubbleDragger.prototype.endBubbleDrag=function(a,b){this.dragBubble(a,b);a=this.pixelsToWorkspaceUnits_(b);a=Blockly.utils.Coordinate.sum(this.startXY_,a);this.draggingBubble_.moveTo(a.x,a.y);this.maybeDeleteBubble_()||(this.dragSurface_&&this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas()),this.draggingBubble_.setDragging&&this.draggingBubble_.setDragging(!1),this.fireMoveEvent_());this.workspace_.setResizesEnabled(!0);this.workspace_.getToolbox()&&(a=this.draggingBubble_.isDeletable()? -"blocklyToolboxDelete":"blocklyToolboxGrab",this.workspace_.getToolbox().removeStyle(a));Blockly.Events.setGroup(!1)};Blockly.BubbleDragger.prototype.fireMoveEvent_=function(){if(this.draggingBubble_.isComment){var a=new Blockly.Events.CommentMove(this.draggingBubble_);a.setOldCoordinate(this.startXY_);a.recordNew();Blockly.Events.fire(a)}}; +Blockly.BubbleDragger.prototype.endBubbleDrag=function(a,b){this.dragBubble(a,b);a=this.pixelsToWorkspaceUnits_(b);a=Blockly.utils.Coordinate.sum(this.startXY_,a);this.draggingBubble_.moveTo(a.x,a.y);this.maybeDeleteBubble_()||(this.dragSurface_&&this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas()),this.draggingBubble_.setDragging&&this.draggingBubble_.setDragging(!1),this.fireMoveEvent_());this.workspace_.setResizesEnabled(!0);(a=this.workspace_.getToolbox())&&"function"==typeof a.removeStyle&& +(b=this.draggingBubble_.isDeletable()?"blocklyToolboxDelete":"blocklyToolboxGrab",a.removeStyle(b));Blockly.Events.setGroup(!1)};Blockly.BubbleDragger.prototype.fireMoveEvent_=function(){if(this.draggingBubble_.isComment){var a=new Blockly.Events.CommentMove(this.draggingBubble_);a.setOldCoordinate(this.startXY_);a.recordNew();Blockly.Events.fire(a)}}; Blockly.BubbleDragger.prototype.pixelsToWorkspaceUnits_=function(a){a=new Blockly.utils.Coordinate(a.x/this.workspace_.scale,a.y/this.workspace_.scale);this.workspace_.isMutator&&a.scale(1/this.workspace_.options.parentWorkspace.scale);return a};Blockly.BubbleDragger.prototype.moveToDragSurface_=function(){this.draggingBubble_.moveTo(0,0);this.dragSurface_.translateSurface(this.startXY_.x,this.startXY_.y);this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot())};Blockly.WorkspaceDragger=function(a){this.workspace_=a;this.startScrollXY_=new Blockly.utils.Coordinate(a.scrollX,a.scrollY)};Blockly.WorkspaceDragger.prototype.dispose=function(){this.workspace_=null};Blockly.WorkspaceDragger.prototype.startDrag=function(){Blockly.selected&&Blockly.selected.unselect();this.workspace_.setupDragSurface()};Blockly.WorkspaceDragger.prototype.endDrag=function(a){this.drag(a);this.workspace_.resetDragSurface()}; -Blockly.WorkspaceDragger.prototype.drag=function(a){a=Blockly.utils.Coordinate.sum(this.startScrollXY_,a);this.workspace_.scroll(a.x,a.y)};Blockly.FlyoutDragger=function(a){Blockly.FlyoutDragger.superClass_.constructor.call(this,a.getWorkspace());this.scrollbar_=a.scrollbar_;this.horizontalLayout_=a.horizontalLayout_};Blockly.utils.object.inherits(Blockly.FlyoutDragger,Blockly.WorkspaceDragger);Blockly.FlyoutDragger.prototype.drag=function(a){a=Blockly.utils.Coordinate.sum(this.startScrollXY_,a);this.horizontalLayout_?this.scrollbar_.set(-a.x):this.scrollbar_.set(-a.y)};Blockly.Action=function(a,b){this.name=a;this.desc=b};Blockly.navigation={};Blockly.navigation.loggingCallback=null;Blockly.navigation.STATE_FLYOUT=1;Blockly.navigation.STATE_WS=2;Blockly.navigation.STATE_TOOLBOX=3;Blockly.navigation.WS_MOVE_DISTANCE=40;Blockly.navigation.currentState_=Blockly.navigation.STATE_WS; -Blockly.navigation.actionNames={PREVIOUS:"previous",NEXT:"next",IN:"in",OUT:"out",INSERT:"insert",MARK:"mark",DISCONNECT:"disconnect",TOOLBOX:"toolbox",EXIT:"exit",TOGGLE_KEYBOARD_NAV:"toggle_keyboard_nav",MOVE_WS_CURSOR_UP:"move workspace cursor up",MOVE_WS_CURSOR_DOWN:"move workspace cursor down",MOVE_WS_CURSOR_LEFT:"move workspace cursor left",MOVE_WS_CURSOR_RIGHT:"move workspace cursor right"};Blockly.navigation.MARKER_NAME="local_marker_1";Blockly.navigation.getMarker=function(){return Blockly.getMainWorkspace().getMarker(Blockly.navigation.MARKER_NAME)}; -Blockly.navigation.focusToolbox_=function(){var a=Blockly.getMainWorkspace().getToolbox();a&&(Blockly.navigation.currentState_=Blockly.navigation.STATE_TOOLBOX,Blockly.navigation.resetFlyout_(!1),Blockly.navigation.getMarker().getCurNode()||Blockly.navigation.markAtCursor_(),a.selectFirstCategory())}; -Blockly.navigation.focusFlyout_=function(){Blockly.navigation.currentState_=Blockly.navigation.STATE_FLYOUT;var a=Blockly.getMainWorkspace();var b=a.getToolbox();a=b?b.flyout_:a.getFlyout();Blockly.navigation.getMarker().getCurNode()||Blockly.navigation.markAtCursor_();a&&a.getWorkspace()&&(a=a.getWorkspace().getTopBlocks(!0),0(this.flyout_?Blockly.FLYOUT_DRAG_RADIUS:Blockly.DRAG_RADIUS)}; -Blockly.Gesture.prototype.updateIsDraggingFromFlyout_=function(){return this.targetBlock_&&this.flyout_.isBlockCreatable_(this.targetBlock_)?!this.flyout_.isScrollable()||this.flyout_.isDragTowardWorkspace(this.currentDragDeltaXY_)?(this.startWorkspace_=this.flyout_.targetWorkspace_,this.startWorkspace_.updateScreenCalculationsIfScrolled(),Blockly.Events.getGroup()||Blockly.Events.setGroup(!0),this.startBlock_=null,this.targetBlock_=this.flyout_.createBlock(this.targetBlock_),this.targetBlock_.select(), +Blockly.Gesture.prototype.updateIsDraggingFromFlyout_=function(){return this.targetBlock_&&this.flyout_.isBlockCreatable_(this.targetBlock_)?!this.flyout_.isScrollable()||this.flyout_.isDragTowardWorkspace(this.currentDragDeltaXY_)?(this.startWorkspace_=this.flyout_.targetWorkspace,this.startWorkspace_.updateScreenCalculationsIfScrolled(),Blockly.Events.getGroup()||Blockly.Events.setGroup(!0),this.startBlock_=null,this.targetBlock_=this.flyout_.createBlock(this.targetBlock_),this.targetBlock_.select(), !0):!1:!1};Blockly.Gesture.prototype.updateIsDraggingBubble_=function(){if(!this.startBubble_)return!1;this.isDraggingBubble_=!0;this.startDraggingBubble_();return!0};Blockly.Gesture.prototype.updateIsDraggingBlock_=function(){if(!this.targetBlock_)return!1;this.flyout_?this.isDraggingBlock_=this.updateIsDraggingFromFlyout_():this.targetBlock_.isMovable()&&(this.isDraggingBlock_=!0);return this.isDraggingBlock_?(this.startDraggingBlock_(),!0):!1}; Blockly.Gesture.prototype.updateIsDraggingWorkspace_=function(){if(this.flyout_?this.flyout_.isScrollable():this.startWorkspace_&&this.startWorkspace_.isDraggable())this.workspaceDragger_=this.flyout_?new Blockly.FlyoutDragger(this.flyout_):new Blockly.WorkspaceDragger(this.startWorkspace_),this.isDraggingWorkspace_=!0,this.workspaceDragger_.startDrag()}; Blockly.Gesture.prototype.updateIsDragging_=function(){if(this.calledUpdateIsDragging_)throw Error("updateIsDragging_ should only be called once per gesture.");this.calledUpdateIsDragging_=!0;this.updateIsDraggingBubble_()||this.updateIsDraggingBlock_()||this.updateIsDraggingWorkspace_()}; Blockly.Gesture.prototype.startDraggingBlock_=function(){this.blockDragger_=new Blockly.BlockDragger(this.targetBlock_,this.startWorkspace_);this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_,this.healStack_);this.blockDragger_.dragBlock(this.mostRecentEvent_,this.currentDragDeltaXY_)}; Blockly.Gesture.prototype.startDraggingBubble_=function(){this.bubbleDragger_=new Blockly.BubbleDragger(this.startBubble_,this.startWorkspace_);this.bubbleDragger_.startBubbleDrag();this.bubbleDragger_.dragBubble(this.mostRecentEvent_,this.currentDragDeltaXY_)}; Blockly.Gesture.prototype.doStart=function(a){Blockly.utils.isTargetInput(a)?this.cancel():(this.hasStarted_=!0,Blockly.blockAnimations.disconnectUiStop(),this.startWorkspace_.updateScreenCalculationsIfScrolled(),this.startWorkspace_.isMutator&&this.startWorkspace_.resize(),Blockly.hideChaff(!!this.flyout_),this.startWorkspace_.markFocused(),this.mostRecentEvent_=a,Blockly.Tooltip.block(),this.targetBlock_&&(!this.targetBlock_.isInFlyout&&a.shiftKey&&this.targetBlock_.workspace.keyboardAccessibilityMode? -this.creatorWorkspace_.getCursor().setCurNode(Blockly.navigation.getTopNode(this.targetBlock_)):this.targetBlock_.select()),Blockly.utils.isRightButton(a)?this.handleRightClick(a):("touchstart"!=a.type.toLowerCase()&&"pointerdown"!=a.type.toLowerCase()||"mouse"==a.pointerType||Blockly.longStart(a,this),this.mouseDownXY_=new Blockly.utils.Coordinate(a.clientX,a.clientY),this.healStack_=a.altKey||a.ctrlKey||a.metaKey,this.bindMouseEvents(a)))}; +this.creatorWorkspace_.getCursor().setCurNode(Blockly.ASTNode.createTopNode(this.targetBlock_)):this.targetBlock_.select()),Blockly.utils.isRightButton(a)?this.handleRightClick(a):("touchstart"!=a.type.toLowerCase()&&"pointerdown"!=a.type.toLowerCase()||"mouse"==a.pointerType||Blockly.longStart(a,this),this.mouseDownXY_=new Blockly.utils.Coordinate(a.clientX,a.clientY),this.healStack_=a.altKey||a.ctrlKey||a.metaKey,this.bindMouseEvents(a)))}; Blockly.Gesture.prototype.bindMouseEvents=function(a){this.onMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",null,this.handleMove.bind(this));this.onUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",null,this.handleUp.bind(this));a.preventDefault();a.stopPropagation()}; Blockly.Gesture.prototype.handleMove=function(a){this.updateFromEvent_(a);this.isDraggingWorkspace_?this.workspaceDragger_.drag(this.currentDragDeltaXY_):this.isDraggingBlock_?this.blockDragger_.dragBlock(this.mostRecentEvent_,this.currentDragDeltaXY_):this.isDraggingBubble_&&this.bubbleDragger_.dragBubble(this.mostRecentEvent_,this.currentDragDeltaXY_);a.preventDefault();a.stopPropagation()}; Blockly.Gesture.prototype.handleUp=function(a){this.updateFromEvent_(a);Blockly.longStop_();this.isEnding_?console.log("Trying to end a gesture recursively."):(this.isEnding_=!0,this.isDraggingBubble_?this.bubbleDragger_.endBubbleDrag(a,this.currentDragDeltaXY_):this.isDraggingBlock_?this.blockDragger_.endBlockDrag(a,this.currentDragDeltaXY_):this.isDraggingWorkspace_?this.workspaceDragger_.endDrag(this.currentDragDeltaXY_):this.isBubbleClick_()?this.doBubbleClick_():this.isFieldClick_()?this.doFieldClick_(): this.isBlockClick_()?this.doBlockClick_():this.isWorkspaceClick_()&&this.doWorkspaceClick_(a),a.preventDefault(),a.stopPropagation(),this.dispose())}; Blockly.Gesture.prototype.cancel=function(){this.isEnding_||(Blockly.longStop_(),this.isDraggingBubble_?this.bubbleDragger_.endBubbleDrag(this.mostRecentEvent_,this.currentDragDeltaXY_):this.isDraggingBlock_?this.blockDragger_.endBlockDrag(this.mostRecentEvent_,this.currentDragDeltaXY_):this.isDraggingWorkspace_&&this.workspaceDragger_.endDrag(this.currentDragDeltaXY_),this.dispose())}; Blockly.Gesture.prototype.handleRightClick=function(a){this.targetBlock_?(this.bringBlockToFront_(),Blockly.hideChaff(!!this.flyout_),this.targetBlock_.showContextMenu(a)):this.startBubble_?this.startBubble_.showContextMenu(a):this.startWorkspace_&&!this.flyout_&&(Blockly.hideChaff(),this.startWorkspace_.showContextMenu(a));a.preventDefault();a.stopPropagation();this.dispose()}; -Blockly.Gesture.prototype.handleWsStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleWsStart, but the gesture had already been started.");this.setStartWorkspace_(b);this.mostRecentEvent_=a;this.doStart(a);this.startWorkspace_.keyboardAccessibilityMode&&Blockly.navigation.setState(Blockly.navigation.STATE_WS)}; +Blockly.Gesture.prototype.handleWsStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleWsStart, but the gesture had already been started.");this.setStartWorkspace_(b);this.mostRecentEvent_=a;this.doStart(a);this.startWorkspace_.keyboardAccessibilityMode&&Blockly.navigation.setState(Blockly.navigation.STATE_WS)};Blockly.Gesture.prototype.fireWorkspaceClick_=function(a){var b=new Blockly.Events.Ui(null,"workspaceClick",null,null);b.workspaceId=a.id;Blockly.Events.fire(b)}; Blockly.Gesture.prototype.handleFlyoutStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleFlyoutStart, but the gesture had already been started.");this.setStartFlyout_(b);this.handleWsStart(a,b.getWorkspace())};Blockly.Gesture.prototype.handleBlockStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleBlockStart, but the gesture had already been started.");this.setStartBlock(b);this.mostRecentEvent_=a}; Blockly.Gesture.prototype.handleBubbleStart=function(a,b){if(this.hasStarted_)throw Error("Tried to call gesture.handleBubbleStart, but the gesture had already been started.");this.setStartBubble(b);this.mostRecentEvent_=a};Blockly.Gesture.prototype.doBubbleClick_=function(){this.startBubble_.setFocus&&this.startBubble_.setFocus();this.startBubble_.select&&this.startBubble_.select()};Blockly.Gesture.prototype.doFieldClick_=function(){this.startField_.showEditor(this.mostRecentEvent_);this.bringBlockToFront_()}; Blockly.Gesture.prototype.doBlockClick_=function(){this.flyout_&&this.flyout_.autoClose?this.targetBlock_.isEnabled()&&(Blockly.Events.getGroup()||Blockly.Events.setGroup(!0),this.flyout_.createBlock(this.targetBlock_).scheduleSnapAndBump()):Blockly.Events.fire(new Blockly.Events.Ui(this.startBlock_,"click",void 0,void 0));this.bringBlockToFront_();Blockly.Events.setGroup(!1)}; -Blockly.Gesture.prototype.doWorkspaceClick_=function(a){var b=this.creatorWorkspace_;a.shiftKey&&b.keyboardAccessibilityMode?(a=new Blockly.utils.Coordinate(a.clientX,a.clientY),a=Blockly.utils.screenToWsCoordinates(b,a),a=Blockly.ASTNode.createWorkspaceNode(b,a),b.getCursor().setCurNode(a)):Blockly.selected&&Blockly.selected.unselect()};Blockly.Gesture.prototype.bringBlockToFront_=function(){this.targetBlock_&&!this.flyout_&&this.targetBlock_.bringToFront()}; +Blockly.Gesture.prototype.doWorkspaceClick_=function(a){var b=this.creatorWorkspace_;a.shiftKey&&b.keyboardAccessibilityMode?(a=new Blockly.utils.Coordinate(a.clientX,a.clientY),a=Blockly.utils.screenToWsCoordinates(b,a),a=Blockly.ASTNode.createWorkspaceNode(b,a),b.getCursor().setCurNode(a)):Blockly.selected&&Blockly.selected.unselect();this.fireWorkspaceClick_(b)};Blockly.Gesture.prototype.bringBlockToFront_=function(){this.targetBlock_&&!this.flyout_&&this.targetBlock_.bringToFront()}; Blockly.Gesture.prototype.setStartField=function(a){if(this.hasStarted_)throw Error("Tried to call gesture.setStartField, but the gesture had already been started.");this.startField_||(this.startField_=a)};Blockly.Gesture.prototype.setStartBubble=function(a){this.startBubble_||(this.startBubble_=a)};Blockly.Gesture.prototype.setStartBlock=function(a){this.startBlock_||this.startBubble_||(this.startBlock_=a,a.isInFlyout&&a!=a.getRootBlock()?this.setTargetBlock_(a.getRootBlock()):this.setTargetBlock_(a))}; Blockly.Gesture.prototype.setTargetBlock_=function(a){a.isShadow()?this.setTargetBlock_(a.getParent()):this.targetBlock_=a};Blockly.Gesture.prototype.setStartWorkspace_=function(a){this.startWorkspace_||(this.startWorkspace_=a)};Blockly.Gesture.prototype.setStartFlyout_=function(a){this.flyout_||(this.flyout_=a)};Blockly.Gesture.prototype.isBubbleClick_=function(){return!!this.startBubble_&&!this.hasExceededDragRadius_}; Blockly.Gesture.prototype.isBlockClick_=function(){return!!this.startBlock_&&!this.hasExceededDragRadius_&&!this.isFieldClick_()};Blockly.Gesture.prototype.isFieldClick_=function(){return(this.startField_?this.startField_.isClickable():!1)&&!this.hasExceededDragRadius_&&(!this.flyout_||!this.flyout_.autoClose)};Blockly.Gesture.prototype.isWorkspaceClick_=function(){return!this.startBlock_&&!this.startBubble_&&!this.startField_&&!this.hasExceededDragRadius_}; -Blockly.Gesture.prototype.isDragging=function(){return this.isDraggingWorkspace_||this.isDraggingBlock_||this.isDraggingBubble_};Blockly.Gesture.prototype.hasStarted=function(){return this.hasStarted_};Blockly.Gesture.prototype.getInsertionMarkers=function(){return this.blockDragger_?this.blockDragger_.getInsertionMarkers():[]};Blockly.Gesture.inProgress=function(){for(var a=Blockly.Workspace.getAll(),b=0,c;c=a[b];b++)if(c.currentGesture_)return!0;return!1};Blockly.Field=function(a,b,c){this.tooltip_=this.validator_=this.value_=null;this.size_=new Blockly.utils.Size(0,0);this.constants_=this.mouseDownWrapper_=this.textContent_=this.textElement_=this.borderRect_=this.fieldGroup_=this.markerSvg_=this.cursorSvg_=null;c&&this.configure_(c);this.setValue(a);b&&this.setValidator(b)};Blockly.Field.prototype.name=void 0;Blockly.Field.prototype.disposed=!1;Blockly.Field.prototype.maxDisplayLength=50;Blockly.Field.prototype.sourceBlock_=null; -Blockly.Field.prototype.isDirty_=!0;Blockly.Field.prototype.visible_=!0;Blockly.Field.prototype.clickTarget_=null;Blockly.Field.NBSP="\u00a0";Blockly.Field.prototype.EDITABLE=!0;Blockly.Field.prototype.SERIALIZABLE=!1;Blockly.Field.prototype.configure_=function(a){var b=a.tooltip;"string"==typeof b&&(b=Blockly.utils.replaceMessageReferences(a.tooltip));b&&this.setTooltip(b)}; +Blockly.Gesture.prototype.isDragging=function(){return this.isDraggingWorkspace_||this.isDraggingBlock_||this.isDraggingBubble_};Blockly.Gesture.prototype.hasStarted=function(){return this.hasStarted_};Blockly.Gesture.prototype.getInsertionMarkers=function(){return this.blockDragger_?this.blockDragger_.getInsertionMarkers():[]};Blockly.Gesture.inProgress=function(){for(var a=Blockly.Workspace.getAll(),b=0,c;c=a[b];b++)if(c.currentGesture_)return!0;return!1};Blockly.Field=function(a,b,c){this.value_=this.DEFAULT_VALUE;this.tooltip_=this.validator_=null;this.size_=new Blockly.utils.Size(0,0);this.constants_=this.mouseDownWrapper_=this.textContent_=this.textElement_=this.borderRect_=this.fieldGroup_=this.markerSvg_=this.cursorSvg_=null;c&&this.configure_(c);this.setValue(a);b&&this.setValidator(b)};Blockly.Field.prototype.DEFAULT_VALUE=null;Blockly.Field.prototype.name=void 0;Blockly.Field.prototype.disposed=!1; +Blockly.Field.prototype.maxDisplayLength=50;Blockly.Field.prototype.sourceBlock_=null;Blockly.Field.prototype.isDirty_=!0;Blockly.Field.prototype.visible_=!0;Blockly.Field.prototype.clickTarget_=null;Blockly.Field.NBSP="\u00a0";Blockly.Field.prototype.EDITABLE=!0;Blockly.Field.prototype.SERIALIZABLE=!1;Blockly.Field.prototype.configure_=function(a){var b=a.tooltip;"string"==typeof b&&(b=Blockly.utils.replaceMessageReferences(a.tooltip));b&&this.setTooltip(b)}; Blockly.Field.prototype.setSourceBlock=function(a){if(this.sourceBlock_)throw Error("Field already bound to a block.");this.sourceBlock_=a};Blockly.Field.prototype.getConstants=function(){!this.constants_&&this.sourceBlock_&&this.sourceBlock_.workspace&&this.sourceBlock_.workspace.rendered&&(this.constants_=this.sourceBlock_.workspace.getRenderer().getConstants());return this.constants_};Blockly.Field.prototype.getSourceBlock=function(){return this.sourceBlock_}; Blockly.Field.prototype.init=function(){this.fieldGroup_||(this.fieldGroup_=Blockly.utils.dom.createSvgElement("g",{},null),this.isVisible()||(this.fieldGroup_.style.display="none"),this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_),this.initView(),this.updateEditable(),this.setTooltip(this.tooltip_),this.bindEvents_(),this.initModel())};Blockly.Field.prototype.initView=function(){this.createBorderRect_();this.createTextElement_()};Blockly.Field.prototype.initModel=function(){}; Blockly.Field.prototype.createBorderRect_=function(){this.borderRect_=Blockly.utils.dom.createSvgElement("rect",{rx:this.getConstants().FIELD_BORDER_RECT_RADIUS,ry:this.getConstants().FIELD_BORDER_RECT_RADIUS,x:0,y:0,height:this.size_.height,width:this.size_.width,"class":"blocklyFieldRect"},this.fieldGroup_)}; @@ -461,24 +476,24 @@ Blockly.Field.prototype.updateSize_=function(a){var b=this.getConstants();a=void Blockly.Field.prototype.positionTextElement_=function(a,b){if(this.textElement_){var c=this.getConstants(),d=this.size_.height/2;this.textElement_.setAttribute("x",this.sourceBlock_.RTL?this.size_.width-b-a:a);this.textElement_.setAttribute("y",c.FIELD_TEXT_BASELINE_CENTER?d:d-c.FIELD_TEXT_HEIGHT/2+c.FIELD_TEXT_BASELINE)}}; Blockly.Field.prototype.positionBorderRect_=function(){this.borderRect_&&(this.borderRect_.setAttribute("width",this.size_.width),this.borderRect_.setAttribute("height",this.size_.height),this.borderRect_.setAttribute("rx",this.getConstants().FIELD_BORDER_RECT_RADIUS),this.borderRect_.setAttribute("ry",this.getConstants().FIELD_BORDER_RECT_RADIUS))}; Blockly.Field.prototype.getSize=function(){if(!this.isVisible())return new Blockly.utils.Size(0,0);this.isDirty_?(this.render_(),this.isDirty_=!1):this.visible_&&0==this.size_.width&&(console.warn("Deprecated use of setting size_.width to 0 to rerender a field. Set field.isDirty_ to true instead."),this.render_());return this.size_}; -Blockly.Field.prototype.getScaledBBox=function(){if(this.borderRect_)a=this.borderRect_.getBoundingClientRect(),c=Blockly.utils.style.getPageOffset(this.borderRect_),d=a.width,a=a.height;else{var a=this.sourceBlock_.getHeightWidth(),b=this.sourceBlock_.workspace.scale,c=this.getAbsoluteXY_(),d=a.width*b;a=a.height*b;Blockly.utils.userAgent.GECKO?(c.x+=1.5*b,c.y+=1.5*b):Blockly.utils.userAgent.EDGE||Blockly.utils.userAgent.IE||(c.x-=.5*b,c.y-=.5*b);d+=1*b;a+=1*b}return{top:c.y,bottom:c.y+a,left:c.x, -right:c.x+d}};Blockly.Field.prototype.getDisplayText_=function(){var a=this.getText();if(!a)return Blockly.Field.NBSP;a.length>this.maxDisplayLength&&(a=a.substring(0,this.maxDisplayLength-2)+"\u2026");a=a.replace(/\s/g,Blockly.Field.NBSP);this.sourceBlock_&&this.sourceBlock_.RTL&&(a+="\u200f");return a};Blockly.Field.prototype.getText=function(){if(this.getText_){var a=this.getText_.call(this);if(null!==a)return String(a)}return String(this.getValue())}; +Blockly.Field.prototype.getScaledBBox=function(){if(this.borderRect_)a=this.borderRect_.getBoundingClientRect(),c=Blockly.utils.style.getPageOffset(this.borderRect_),d=a.width,a=a.height;else{var a=this.sourceBlock_.getHeightWidth(),b=this.sourceBlock_.workspace.scale,c=this.getAbsoluteXY_(),d=a.width*b;a=a.height*b;Blockly.utils.userAgent.GECKO?(c.x+=1.5*b,c.y+=1.5*b):Blockly.utils.userAgent.EDGE||Blockly.utils.userAgent.IE||(c.x-=.5*b,c.y-=.5*b);d+=1*b;a+=1*b}return new Blockly.utils.Rect(c.y,c.y+ +a,c.x,c.x+d)};Blockly.Field.prototype.getDisplayText_=function(){var a=this.getText();if(!a)return Blockly.Field.NBSP;a.length>this.maxDisplayLength&&(a=a.substring(0,this.maxDisplayLength-2)+"\u2026");a=a.replace(/\s/g,Blockly.Field.NBSP);this.sourceBlock_&&this.sourceBlock_.RTL&&(a+="\u200f");return a};Blockly.Field.prototype.getText=function(){if(this.getText_){var a=this.getText_.call(this);if(null!==a)return String(a)}return String(this.getValue())}; Blockly.Field.prototype.setText=function(a){throw Error("setText method is deprecated");};Blockly.Field.prototype.markDirty=function(){this.isDirty_=!0;this.constants_=null};Blockly.Field.prototype.forceRerender=function(){this.isDirty_=!0;this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours(),this.updateMarkers_())}; -Blockly.Field.prototype.setValue=function(a){if(null!==a){var b=this.doClassValidation_(a);a=this.processValidation_(a,b);if(!(a instanceof Error)){if(b=this.getValidator())if(b=b.call(this,a),a=this.processValidation_(a,b),a instanceof Error)return;b=this.getValue();b!==a&&(this.sourceBlock_&&Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockChange(this.sourceBlock_,"field",this.name||null,b,a)),this.doValueUpdate_(a),this.isDirty_&&this.forceRerender())}}}; +Blockly.Field.prototype.setValue=function(a){if(null!==a){var b=this.doClassValidation_(a);a=this.processValidation_(a,b);if(!(a instanceof Error)){if(b=this.getValidator())if(b=b.call(this,a),a=this.processValidation_(a,b),a instanceof Error)return;b=this.sourceBlock_;if(!b||!b.disposed){var c=this.getValue();c!==a&&(b&&Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockChange(b,"field",this.name||null,c,a)),this.doValueUpdate_(a),this.isDirty_&&this.forceRerender())}}}}; Blockly.Field.prototype.processValidation_=function(a,b){if(null===b)return this.doValueInvalid_(a),this.isDirty_&&this.forceRerender(),Error();void 0!==b&&(a=b);return a};Blockly.Field.prototype.getValue=function(){return this.value_};Blockly.Field.prototype.doClassValidation_=function(a){return null===a||void 0===a?null:a=this.classValidator(a)};Blockly.Field.prototype.doValueUpdate_=function(a){this.value_=a;this.isDirty_=!0};Blockly.Field.prototype.doValueInvalid_=function(a){}; Blockly.Field.prototype.onMouseDown_=function(a){this.sourceBlock_&&this.sourceBlock_.workspace&&(a=this.sourceBlock_.workspace.getGesture(a))&&a.setStartField(this)};Blockly.Field.prototype.setTooltip=function(a){var b=this.getClickTarget_();b?b.tooltip=a||""===a?a:this.sourceBlock_:this.tooltip_=a};Blockly.Field.prototype.getClickTarget_=function(){return this.clickTarget_||this.getSvgRoot()};Blockly.Field.prototype.getAbsoluteXY_=function(){return Blockly.utils.style.getPageOffset(this.getClickTarget_())}; Blockly.Field.prototype.referencesVariables=function(){return!1};Blockly.Field.prototype.getParentInput=function(){for(var a=null,b=this.sourceBlock_,c=b.inputList,d=0;da||a>this.fieldRow.length)throw Error("index "+a+" out of bounds.");if(!(b||""==b&&c))return a;"string"==typeof b&&(b=new Blockly.FieldLabel(b));b.setSourceBlock(this.sourceBlock_);this.sourceBlock_.rendered&&b.init();b.name=c;b.prefixField&&(a=this.insertFieldAt(a,b.prefixField));this.fieldRow.splice(a,0,b);++a;b.suffixField&&(a=this.insertFieldAt(a,b.suffixField));this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours()); -return a};Blockly.Input.prototype.removeField=function(a){for(var b=0,c;c=this.fieldRow[b];b++)if(c.name===a){c.dispose();this.fieldRow.splice(b,1);this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours());return}throw Error('Field "%s" not found.',a);};Blockly.Input.prototype.isVisible=function(){return this.visible_}; -Blockly.Input.prototype.setVisible=function(a){var b=[];if(this.visible_==a)return b;for(var c=(this.visible_=a)?"block":"none",d=0,e;e=this.fieldRow[d];d++)e.setVisible(a);this.connection&&(a?b=this.connection.startTrackingAll():this.connection.stopTrackingAll(),d=this.connection.targetBlock())&&(d.getSvgRoot().style.display=c,a||(d.rendered=!1));return b};Blockly.Input.prototype.markDirty=function(){for(var a=0,b;b=this.fieldRow[a];a++)b.markDirty()}; -Blockly.Input.prototype.setCheck=function(a){if(!this.connection)throw Error("This input does not have a connection.");this.connection.setCheck(a);return this};Blockly.Input.prototype.setAlign=function(a){this.align=a;this.sourceBlock_.rendered&&this.sourceBlock_.render();return this};Blockly.Input.prototype.init=function(){if(this.sourceBlock_.workspace.rendered)for(var a=0;aa||a>this.fieldRow.length)throw Error("index "+a+" out of bounds.");if(!(b||""==b&&c))return a;"string"==typeof b&&(b=new Blockly.FieldLabel(b));b.setSourceBlock(this.sourceBlock_);this.sourceBlock_.rendered&&b.init();b.name=c;b.setVisible(this.isVisible());c=b;c.prefixField&&(a=this.insertFieldAt(a,c.prefixField));this.fieldRow.splice(a,0,b);++a;c.suffixField&&(a=this.insertFieldAt(a,c.suffixField));this.sourceBlock_.rendered&&(this.sourceBlock_= +this.sourceBlock_,this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours());return a};Blockly.Input.prototype.removeField=function(a,b){for(var c=0,d;d=this.fieldRow[c];c++)if(d.name===a)return d.dispose(),this.fieldRow.splice(c,1),this.sourceBlock_.rendered&&(this.sourceBlock_=this.sourceBlock_,this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours()),!0;if(b)return!1;throw Error('Field "'+a+'" not found.');};Blockly.Input.prototype.isVisible=function(){return this.visible_}; +Blockly.Input.prototype.setVisible=function(a){var b=[];if(this.visible_==a)return b;this.visible_=a;for(var c=0,d;d=this.fieldRow[c];c++)d.setVisible(a);this.connection&&(this.connection=this.connection,a?b=this.connection.startTrackingAll():this.connection.stopTrackingAll(),c=this.connection.targetBlock())&&(c.getSvgRoot().style.display=a?"block":"none");return b};Blockly.Input.prototype.markDirty=function(){for(var a=0,b;b=this.fieldRow[a];a++)b.markDirty()}; +Blockly.Input.prototype.setCheck=function(a){if(!this.connection)throw Error("This input does not have a connection.");this.connection.setCheck(a);return this};Blockly.Input.prototype.setAlign=function(a){this.align=a;this.sourceBlock_.rendered&&(this.sourceBlock_=this.sourceBlock_,this.sourceBlock_.render());return this};Blockly.Input.prototype.init=function(){if(this.sourceBlock_.workspace.rendered)for(var a=0;aa&&(c=c.substring(0,a-3)+"...");return c}; -Blockly.Block.prototype.appendValueInput=function(a){return this.appendInput_(Blockly.INPUT_VALUE,a)};Blockly.Block.prototype.appendStatementInput=function(a){return this.appendInput_(Blockly.NEXT_STATEMENT,a)};Blockly.Block.prototype.appendDummyInput=function(a){return this.appendInput_(Blockly.DUMMY_INPUT,a||"")}; +Blockly.Block.prototype.toString=function(a,b){function c(a){var b=a.getCheck();!b&&a.targetConnection&&(b=a.targetConnection.getCheck());return!!b&&(-1!=b.indexOf("Boolean")||-1!=b.indexOf("Number"))}function d(){g&&g.getType()==h.getType()&&g.getLocation()==h.getLocation()&&(g=null)}var e=[];b=b||"?";var f=Blockly.ASTNode.NAVIGATE_ALL_FIELDS;Blockly.ASTNode.NAVIGATE_ALL_FIELDS=!0;for(var g=Blockly.ASTNode.createBlockNode(this),h=g;g;){switch(g.getType()){case Blockly.ASTNode.types.INPUT:var k=g.getLocation(); +g.in()?c(k)&&e.push("("):e.push(b);break;case Blockly.ASTNode.types.FIELD:k=g.getLocation(),k.name!=Blockly.Block.COLLAPSED_FIELD_NAME&&e.push(k.getText())}k=g;g=k.in()||k.next();if(!g){g=k.out();for(d();g&&!g.next();)g=g.out(),d(),g&&g.getType()==Blockly.ASTNode.types.INPUT&&c(g.getLocation())&&e.push(")");g&&(g=g.next())}}Blockly.ASTNode.NAVIGATE_ALL_FIELDS=f;b=2;for(f=e.length;ba&&(e=e.substring(0,a-3)+"...");return e};Blockly.Block.prototype.appendValueInput=function(a){return this.appendInput_(Blockly.INPUT_VALUE,a)};Blockly.Block.prototype.appendStatementInput=function(a){return this.appendInput_(Blockly.NEXT_STATEMENT,a)};Blockly.Block.prototype.appendDummyInput=function(a){return this.appendInput_(Blockly.DUMMY_INPUT,a||"")}; Blockly.Block.prototype.jsonInit=function(a){var b=a.type?'Block "'+a.type+'": ':"";if(a.output&&a.previousStatement)throw Error(b+"Must not have both an output and a previousStatement.");a.style&&a.style.hat&&(this.hat=a.style.hat,a.style=null);if(a.style&&a.colour)throw Error(b+"Must not have both a colour and a style.");a.style?this.jsonInitStyle_(a,b):this.jsonInitColour_(a,b);for(var c=0;void 0!==a["message"+c];)this.interpolate_(a["message"+c],a["args"+c]||[],a["lastDummyAlign"+c],b),c++;void 0!== a.inputsInline&&this.setInputsInline(a.inputsInline);void 0!==a.output&&this.setOutput(!0,a.output);void 0!==a.outputShape&&this.setOutputShape(a.outputShape);void 0!==a.previousStatement&&this.setPreviousStatement(!0,a.previousStatement);void 0!==a.nextStatement&&this.setNextStatement(!0,a.nextStatement);void 0!==a.tooltip&&(c=a.tooltip,c=Blockly.utils.replaceMessageReferences(c),this.setTooltip(c));void 0!==a.enableContextMenu&&(c=a.enableContextMenu,this.contextMenu=!!c);void 0!==a.helpUrl&&(c= a.helpUrl,c=Blockly.utils.replaceMessageReferences(c),this.setHelpUrl(c));"string"==typeof a.extensions&&(console.warn(b+"JSON attribute 'extensions' should be an array of strings. Found raw string in JSON for '"+a.type+"' block."),a.extensions=[a.extensions]);void 0!==a.mutator&&Blockly.Extensions.apply(a.mutator,this,!0);if(Array.isArray(a.extensions))for(a=a.extensions,b=0;b=this.inputList.length)throw RangeError("Input index "+a+" out of bounds.");if(b>this.inputList.length)throw RangeError("Reference input "+b+" out of bounds.");var c=this.inputList[a];this.inputList.splice(a,1);aa?b-1:a},this.highlightedIndex_)}; -Blockly.Menu.prototype.highlightHelper=function(a,b){b=0>b?-1:b;var c=this.getChildCount();b=a.call(this,b,c);for(var d=0;d<=c;){var e=this.getChildAt(b);if(e&&this.canHighlightItem(e))return this.setHighlightedIndex(b),!0;d++;b=a.call(this,b,c)}return!1};Blockly.Menu.prototype.canHighlightItem=function(a){return a.isEnabled()}; -Blockly.Menu.prototype.handleMouseOver_=function(a){if(a=this.getMenuItem(a.target))a.isEnabled()?this.getHighlighted()!==a&&(this.unhighlightCurrent(),this.setHighlighted(a)):this.unhighlightCurrent()};Blockly.Menu.prototype.handleClick_=function(a){var b=this.openingCoords;this.openingCoords=null;if(b&&"number"===typeof a.clientX){var c=new Blockly.utils.Coordinate(a.clientX,a.clientY);if(1>Blockly.utils.Coordinate.distance(b,c))return}(b=this.getMenuItem(a.target))&&b.handleClick(a)&&a.preventDefault()}; -Blockly.Menu.prototype.handleMouseEnter_=function(a){this.focus()};Blockly.Menu.prototype.handleMouseLeave_=function(a){this.getElement()&&(this.blur(),this.clearHighlighted())};Blockly.Menu.prototype.handleKeyEvent=function(a){return 0!=this.getChildCount()&&this.handleKeyEventInternal(a)?(a.preventDefault(),a.stopPropagation(),!0):!1}; -Blockly.Menu.prototype.handleKeyEventInternal=function(a){var b=this.getHighlighted();if(b&&"function"==typeof b.handleKeyEvent&&b.handleKeyEvent(a))return!0;if(a.shiftKey||a.ctrlKey||a.metaKey||a.altKey)return!1;switch(a.keyCode){case Blockly.utils.KeyCodes.ENTER:b&&b.performActionInternal(a);break;case Blockly.utils.KeyCodes.UP:this.highlightPrevious();break;case Blockly.utils.KeyCodes.DOWN:this.highlightNext();break;default:return!1}return!0};Blockly.MenuItem=function(a,b){Blockly.Component.call(this);this.setContentInternal(a);this.setValue(b);this.enabled_=!0};Blockly.utils.object.inherits(Blockly.MenuItem,Blockly.Component); -Blockly.MenuItem.prototype.createDom=function(){var a=document.createElement("div");a.id=this.getId();this.setElementInternal(a);a.className="goog-menuitem goog-option "+(this.enabled_?"":"goog-menuitem-disabled ")+(this.checked_?"goog-option-selected ":"")+(this.rightToLeft_?"goog-menuitem-rtl ":"");var b=this.getContentWrapperDom();a.appendChild(b);var c=this.getCheckboxDom();c&&b.appendChild(c);b.appendChild(this.getContentDom());Blockly.utils.aria.setRole(a,this.roleName_||(this.checkable_?Blockly.utils.aria.Role.MENUITEMCHECKBOX: -Blockly.utils.aria.Role.MENUITEM));Blockly.utils.aria.setState(a,Blockly.utils.aria.State.SELECTED,this.checkable_&&this.checked_||!1)};Blockly.MenuItem.prototype.getCheckboxDom=function(){if(!this.checkable_)return null;var a=document.createElement("div");a.className="goog-menuitem-checkbox";return a};Blockly.MenuItem.prototype.getContentDom=function(){var a=this.content_;"string"===typeof a&&(a=document.createTextNode(a));return a}; -Blockly.MenuItem.prototype.getContentWrapperDom=function(){var a=document.createElement("div");a.className="goog-menuitem-content";return a};Blockly.MenuItem.prototype.setContentInternal=function(a){this.content_=a};Blockly.MenuItem.prototype.setValue=function(a){this.value_=a};Blockly.MenuItem.prototype.getValue=function(){return this.value_};Blockly.MenuItem.prototype.setRole=function(a){this.roleName_=a};Blockly.MenuItem.prototype.setCheckable=function(a){this.checkable_=a}; -Blockly.MenuItem.prototype.setChecked=function(a){if(this.checkable_){this.checked_=a;var b=this.getElement();b&&this.isEnabled()&&(a?(Blockly.utils.dom.addClass(b,"goog-option-selected"),Blockly.utils.aria.setState(b,Blockly.utils.aria.State.SELECTED,!0)):(Blockly.utils.dom.removeClass(b,"goog-option-selected"),Blockly.utils.aria.setState(b,Blockly.utils.aria.State.SELECTED,!1)))}}; -Blockly.MenuItem.prototype.setHighlighted=function(a){this.highlight_=a;var b=this.getElement();b&&this.isEnabled()&&(a?Blockly.utils.dom.addClass(b,"goog-menuitem-highlight"):Blockly.utils.dom.removeClass(b,"goog-menuitem-highlight"))};Blockly.MenuItem.prototype.isEnabled=function(){return this.enabled_};Blockly.MenuItem.prototype.setEnabled=function(a){this.enabled_=a;(a=this.getElement())&&(this.enabled_?Blockly.utils.dom.removeClass(a,"goog-menuitem-disabled"):Blockly.utils.dom.addClass(a,"goog-menuitem-disabled"))}; -Blockly.MenuItem.prototype.handleClick=function(a){this.isEnabled()&&(this.setHighlighted(!0),this.performActionInternal())};Blockly.MenuItem.prototype.performActionInternal=function(){this.checkable_&&this.setChecked(!this.checked_);this.actionHandler_&&this.actionHandler_.call(this.actionHandlerObj_,this)};Blockly.MenuItem.prototype.onAction=function(a,b){this.actionHandler_=a;this.actionHandlerObj_=b};Blockly.utils.uiMenu={};Blockly.utils.uiMenu.getSize=function(a){a=a.getElement();var b=Blockly.utils.style.getSize(a);b.height=a.scrollHeight;return b};Blockly.utils.uiMenu.adjustBBoxesForRTL=function(a,b,c){b.left+=c.width;b.right+=c.width;a.left+=c.width;a.right+=c.width};Blockly.ContextMenu={};Blockly.ContextMenu.currentBlock=null;Blockly.ContextMenu.eventWrapper_=null;Blockly.ContextMenu.show=function(a,b,c){Blockly.WidgetDiv.show(Blockly.ContextMenu,c,null);if(b.length){var d=Blockly.ContextMenu.populate_(b,c);Blockly.ContextMenu.position_(d,a,c);setTimeout(function(){d.getElement().focus()},1);Blockly.ContextMenu.currentBlock=null}else Blockly.ContextMenu.hide()}; -Blockly.ContextMenu.populate_=function(a,b){var c=new Blockly.Menu;c.setRightToLeft(b);for(var d=0,e;e=a[d];d++){var f=new Blockly.MenuItem(e.text);f.setRightToLeft(b);c.addChild(f,!0);f.setEnabled(e.enabled);if(e.enabled)f.onAction(function(){Blockly.ContextMenu.hide();this.callback()},e)}return c}; -Blockly.ContextMenu.position_=function(a,b,c){var d=Blockly.utils.getViewportBBox();b={top:b.clientY+d.top,bottom:b.clientY+d.top,left:b.clientX+d.left,right:b.clientX+d.left};Blockly.ContextMenu.createWidget_(a);var e=Blockly.utils.uiMenu.getSize(a);c&&Blockly.utils.uiMenu.adjustBBoxesForRTL(d,b,e);Blockly.WidgetDiv.positionWithAnchor(d,b,e,c);a.getElement().focus()}; -Blockly.ContextMenu.createWidget_=function(a){a.render(Blockly.WidgetDiv.DIV);var b=a.getElement();Blockly.utils.dom.addClass(b,"blocklyContextMenu");Blockly.bindEventWithChecks_(b,"contextmenu",null,Blockly.utils.noEvent);a.focus()};Blockly.ContextMenu.hide=function(){Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu);Blockly.ContextMenu.currentBlock=null;Blockly.ContextMenu.eventWrapper_&&(Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_),Blockly.ContextMenu.eventWrapper_=null)}; +Blockly.utils.aria.State={ACTIVEDESCENDANT:"activedescendant",COLCOUNT:"colcount",DISABLED:"disabled",EXPANDED:"expanded",INVALID:"invalid",LABEL:"label",LABELLEDBY:"labelledby",LEVEL:"level",ORIENTATION:"orientation",POSINSET:"posinset",ROWCOUNT:"rowcount",SELECTED:"selected",SETSIZE:"setsize",VALUEMAX:"valuemax",VALUEMIN:"valuemin"};Blockly.utils.aria.setRole=function(a,b){a.setAttribute(Blockly.utils.aria.ROLE_ATTRIBUTE_,b)}; +Blockly.utils.aria.setState=function(a,b,c){Array.isArray(c)&&(c=c.join(" "));a.setAttribute(Blockly.utils.aria.ARIA_PREFIX_+b,c)};Blockly.Menu=function(){this.menuItems_=[];this.roleName_=this.element_=this.onKeyDownHandler_=this.mouseLeaveHandler_=this.mouseEnterHandler_=this.clickHandler_=this.mouseOverHandler_=this.highlightedItem_=this.openingCoords=null};Blockly.Menu.prototype.addChild=function(a){this.menuItems_.push(a)}; +Blockly.Menu.prototype.render=function(a){var b=document.createElement("div");b.className="blocklyMenu goog-menu blocklyNonSelectable";b.tabIndex=0;this.roleName_&&Blockly.utils.aria.setRole(b,this.roleName_);this.element_=b;for(var c=0,d;d=this.menuItems_[c];c++)b.appendChild(d.createDom());this.mouseOverHandler_=Blockly.bindEventWithChecks_(b,"mouseover",this,this.handleMouseOver_,!0);this.clickHandler_=Blockly.bindEventWithChecks_(b,"click",this,this.handleClick_,!0);this.mouseEnterHandler_=Blockly.bindEventWithChecks_(b, +"mouseenter",this,this.handleMouseEnter_,!0);this.mouseLeaveHandler_=Blockly.bindEventWithChecks_(b,"mouseleave",this,this.handleMouseLeave_,!0);this.onKeyDownHandler_=Blockly.bindEventWithChecks_(b,"keydown",this,this.handleKeyEvent_);a.appendChild(b)};Blockly.Menu.prototype.getElement=function(){return this.element_};Blockly.Menu.prototype.focus=function(){var a=this.getElement();a&&(a.focus({preventScroll:!0}),Blockly.utils.dom.addClass(a,"blocklyFocused"))}; +Blockly.Menu.prototype.blur_=function(){var a=this.getElement();a&&(a.blur(),Blockly.utils.dom.removeClass(a,"blocklyFocused"))};Blockly.Menu.prototype.setRole=function(a){this.roleName_=a}; +Blockly.Menu.prototype.dispose=function(){this.mouseOverHandler_&&(Blockly.unbindEvent_(this.mouseOverHandler_),this.mouseOverHandler_=null);this.clickHandler_&&(Blockly.unbindEvent_(this.clickHandler_),this.clickHandler_=null);this.mouseEnterHandler_&&(Blockly.unbindEvent_(this.mouseEnterHandler_),this.mouseEnterHandler_=null);this.mouseLeaveHandler_&&(Blockly.unbindEvent_(this.mouseLeaveHandler_),this.mouseLeaveHandler_=null);this.onKeyDownHandler_&&(Blockly.unbindEvent_(this.onKeyDownHandler_), +this.onKeyDownHandler_=null);for(var a=0,b;b=this.menuItems_[a];a++)b.dispose();this.element_=null};Blockly.Menu.prototype.getMenuItem_=function(a){for(var b=this.getElement();a&&a!=b;){if(Blockly.utils.dom.hasClass(a,"blocklyMenuItem"))for(var c=0,d;d=this.menuItems_[c];c++)if(d.getElement()==a)return d;a=a.parentElement}return null}; +Blockly.Menu.prototype.setHighlighted=function(a){var b=this.highlightedItem_;b&&(b.setHighlighted(!1),this.highlightedItem_=null);a&&(a.setHighlighted(!0),this.highlightedItem_=a,b=this.getElement(),Blockly.utils.style.scrollIntoContainerView(a.getElement(),b),Blockly.utils.aria.setState(b,Blockly.utils.aria.State.ACTIVEDESCENDANT,a.getId()))};Blockly.Menu.prototype.highlightNext=function(){var a=this.menuItems_.indexOf(this.highlightedItem_);this.highlightHelper_(a,1)}; +Blockly.Menu.prototype.highlightPrevious=function(){var a=this.menuItems_.indexOf(this.highlightedItem_);this.highlightHelper_(0>a?this.menuItems_.length:a,-1)};Blockly.Menu.prototype.highlightFirst_=function(){this.highlightHelper_(-1,1)};Blockly.Menu.prototype.highlightLast_=function(){this.highlightHelper_(this.menuItems_.length,-1)};Blockly.Menu.prototype.highlightHelper_=function(a,b){a+=b;for(var c;c=this.menuItems_[a];){if(c.isEnabled()){this.setHighlighted(c);break}a+=b}}; +Blockly.Menu.prototype.handleMouseOver_=function(a){(a=this.getMenuItem_(a.target))&&(a.isEnabled()?this.highlightedItem_!=a&&this.setHighlighted(a):this.setHighlighted(null))};Blockly.Menu.prototype.handleClick_=function(a){var b=this.openingCoords;this.openingCoords=null;if(b&&"number"==typeof a.clientX){var c=new Blockly.utils.Coordinate(a.clientX,a.clientY);if(1>Blockly.utils.Coordinate.distance(b,c))return}(a=this.getMenuItem_(a.target))&&a.performAction()}; +Blockly.Menu.prototype.handleMouseEnter_=function(a){this.focus()};Blockly.Menu.prototype.handleMouseLeave_=function(a){this.getElement()&&(this.blur_(),this.setHighlighted(null))}; +Blockly.Menu.prototype.handleKeyEvent_=function(a){if(this.menuItems_.length&&!(a.shiftKey||a.ctrlKey||a.metaKey||a.altKey)){var b=this.highlightedItem_;switch(a.keyCode){case Blockly.utils.KeyCodes.ENTER:case Blockly.utils.KeyCodes.SPACE:b&&b.performAction();break;case Blockly.utils.KeyCodes.UP:this.highlightPrevious();break;case Blockly.utils.KeyCodes.DOWN:this.highlightNext();break;case Blockly.utils.KeyCodes.PAGE_UP:case Blockly.utils.KeyCodes.HOME:this.highlightFirst_();break;case Blockly.utils.KeyCodes.PAGE_DOWN:case Blockly.utils.KeyCodes.END:this.highlightLast_(); +break;default:return}a.preventDefault();a.stopPropagation()}};Blockly.Menu.prototype.getSize=function(){var a=this.getElement(),b=Blockly.utils.style.getSize(a);b.height=a.scrollHeight;return b};Blockly.MenuItem=function(a,b){this.content_=a;this.value_=b;this.enabled_=!0;this.element_=null;this.rightToLeft_=!1;this.roleName_=null;this.highlight_=this.checked_=this.checkable_=!1;this.actionHandler_=null}; +Blockly.MenuItem.prototype.createDom=function(){var a=document.createElement("div");a.id=Blockly.utils.IdGenerator.getNextUniqueId();this.element_=a;a.className="blocklyMenuItem goog-menuitem "+(this.enabled_?"":"blocklyMenuItemDisabled goog-menuitem-disabled ")+(this.checked_?"blocklyMenuItemSelected goog-option-selected ":"")+(this.highlight_?"blocklyMenuItemHighlight goog-menuitem-highlight ":"")+(this.rightToLeft_?"blocklyMenuItemRtl goog-menuitem-rtl ":"");var b=document.createElement("div"); +b.className="blocklyMenuItemContent goog-menuitem-content";if(this.checkable_){var c=document.createElement("div");c.className="blocklyMenuItemCheckbox goog-menuitem-checkbox";b.appendChild(c)}b.appendChild(document.createTextNode(this.content_));a.appendChild(b);this.roleName_&&Blockly.utils.aria.setRole(a,this.roleName_);Blockly.utils.aria.setState(a,Blockly.utils.aria.State.SELECTED,this.checkable_&&this.checked_||!1);Blockly.utils.aria.setState(a,Blockly.utils.aria.State.DISABLED,!this.enabled_); +return a};Blockly.MenuItem.prototype.dispose=function(){this.element_=null};Blockly.MenuItem.prototype.getElement=function(){return this.element_};Blockly.MenuItem.prototype.getId=function(){return this.element_.id};Blockly.MenuItem.prototype.getValue=function(){return this.value_};Blockly.MenuItem.prototype.setRightToLeft=function(a){this.rightToLeft_=a};Blockly.MenuItem.prototype.setRole=function(a){this.roleName_=a};Blockly.MenuItem.prototype.setCheckable=function(a){this.checkable_=a}; +Blockly.MenuItem.prototype.setChecked=function(a){this.checked_=a};Blockly.MenuItem.prototype.setHighlighted=function(a){this.highlight_=a;var b=this.getElement();b&&this.isEnabled()&&(a?(Blockly.utils.dom.addClass(b,"blocklyMenuItemHighlight"),Blockly.utils.dom.addClass(b,"goog-menuitem-highlight")):(Blockly.utils.dom.removeClass(b,"blocklyMenuItemHighlight"),Blockly.utils.dom.removeClass(b,"goog-menuitem-highlight")))};Blockly.MenuItem.prototype.isEnabled=function(){return this.enabled_}; +Blockly.MenuItem.prototype.setEnabled=function(a){this.enabled_=a};Blockly.MenuItem.prototype.performAction=function(){this.isEnabled()&&this.actionHandler_&&this.actionHandler_(this)};Blockly.MenuItem.prototype.onAction=function(a,b){this.actionHandler_=a.bind(b)};Blockly.ContextMenu={};Blockly.ContextMenu.currentBlock=null;Blockly.ContextMenu.menu_=null;Blockly.ContextMenu.show=function(a,b,c){Blockly.WidgetDiv.show(Blockly.ContextMenu,c,Blockly.ContextMenu.dispose);if(b.length){var d=Blockly.ContextMenu.populate_(b,c);Blockly.ContextMenu.menu_=d;Blockly.ContextMenu.position_(d,a,c);setTimeout(function(){d.focus()},1);Blockly.ContextMenu.currentBlock=null}else Blockly.ContextMenu.hide()}; +Blockly.ContextMenu.populate_=function(a,b){var c=new Blockly.Menu;c.setRole(Blockly.utils.aria.Role.MENU);for(var d=0,e;e=a[d];d++){var f=new Blockly.MenuItem(e.text);f.setRightToLeft(b);f.setRole(Blockly.utils.aria.Role.MENUITEM);c.addChild(f);f.setEnabled(e.enabled);if(e.enabled)f.onAction(function(a){Blockly.ContextMenu.hide();this.callback()},e)}return c}; +Blockly.ContextMenu.position_=function(a,b,c){var d=Blockly.utils.getViewportBBox();b=new Blockly.utils.Rect(b.clientY+d.top,b.clientY+d.top,b.clientX+d.left,b.clientX+d.left);Blockly.ContextMenu.createWidget_(a);var e=a.getSize();c&&(b.left+=e.width,b.right+=e.width,d.left+=e.width,d.right+=e.width);Blockly.WidgetDiv.positionWithAnchor(d,b,e,c);a.focus()}; +Blockly.ContextMenu.createWidget_=function(a){a.render(Blockly.WidgetDiv.DIV);var b=a.getElement();Blockly.utils.dom.addClass(b,"blocklyContextMenu");Blockly.bindEventWithChecks_(b,"contextmenu",null,Blockly.utils.noEvent);a.focus()};Blockly.ContextMenu.hide=function(){Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu);Blockly.ContextMenu.currentBlock=null};Blockly.ContextMenu.dispose=function(){Blockly.ContextMenu.menu_&&(Blockly.ContextMenu.menu_.dispose(),Blockly.ContextMenu.menu_=null)}; Blockly.ContextMenu.callbackFactory=function(a,b){return function(){Blockly.Events.disable();try{var c=Blockly.Xml.domToBlock(b,a.workspace),d=a.getRelativeToSurfaceXY();d.x=a.RTL?d.x-Blockly.SNAP_RADIUS:d.x+Blockly.SNAP_RADIUS;d.y+=2*Blockly.SNAP_RADIUS;c.moveBy(d.x,d.y)}finally{Blockly.Events.enable()}Blockly.Events.isEnabled()&&!c.isShadow()&&Blockly.Events.fire(new Blockly.Events.BlockCreate(c));c.select()}}; Blockly.ContextMenu.blockDeleteOption=function(a){var b=a.getDescendants(!1).length,c=a.getNextBlock();c&&(b-=c.getDescendants(!1).length);return{text:1==b?Blockly.Msg.DELETE_BLOCK:Blockly.Msg.DELETE_X_BLOCKS.replace("%1",String(b)),enabled:!0,callback:function(){Blockly.Events.setGroup(!0);a.dispose(!0,!0);Blockly.Events.setGroup(!1)}}};Blockly.ContextMenu.blockHelpOption=function(a){return{enabled:!("function"==typeof a.helpUrl?!a.helpUrl():!a.helpUrl),text:Blockly.Msg.HELP,callback:function(){a.showHelp()}}}; Blockly.ContextMenu.blockDuplicateOption=function(a){var b=a.isDuplicatable();return{text:Blockly.Msg.DUPLICATE_BLOCK,enabled:b,callback:function(){Blockly.duplicate(a)}}};Blockly.ContextMenu.blockCommentOption=function(a){var b={enabled:!Blockly.utils.userAgent.IE};a.getCommentIcon()?(b.text=Blockly.Msg.REMOVE_COMMENT,b.callback=function(){a.setCommentText(null)}):(b.text=Blockly.Msg.ADD_COMMENT,b.callback=function(){a.setCommentText("")});return b}; Blockly.ContextMenu.commentDeleteOption=function(a){return{text:Blockly.Msg.REMOVE_COMMENT,enabled:!0,callback:function(){Blockly.Events.setGroup(!0);a.dispose(!0,!0);Blockly.Events.setGroup(!1)}}};Blockly.ContextMenu.commentDuplicateOption=function(a){return{text:Blockly.Msg.DUPLICATE_COMMENT,enabled:!0,callback:function(){Blockly.duplicate(a)}}}; Blockly.ContextMenu.workspaceCommentOption=function(a,b){if(!Blockly.WorkspaceCommentSvg)throw Error("Missing require for Blockly.WorkspaceCommentSvg");var c={enabled:!Blockly.utils.userAgent.IE};c.text=Blockly.Msg.ADD_COMMENT;c.callback=function(){var c=new Blockly.WorkspaceCommentSvg(a,Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT,Blockly.WorkspaceCommentSvg.DEFAULT_SIZE,Blockly.WorkspaceCommentSvg.DEFAULT_SIZE),e=a.getInjectionDiv().getBoundingClientRect();e=new Blockly.utils.Coordinate(b.clientX- -e.left,b.clientY-e.top);var f=a.getOriginOffsetInPixels();e=Blockly.utils.Coordinate.difference(e,f);e.scale(1/a.scale);c.moveBy(e.x,e.y);a.rendered&&(c.initSvg(),c.render(),c.select())};return c};Blockly.RenderedConnection=function(a,b){Blockly.RenderedConnection.superClass_.constructor.call(this,a,b);this.db_=a.workspace.connectionDBList[b];this.dbOpposite_=a.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[b]];this.offsetInBlock_=new Blockly.utils.Coordinate(0,0);this.trackedState_=Blockly.RenderedConnection.TrackedState.WILL_TRACK};Blockly.utils.object.inherits(Blockly.RenderedConnection,Blockly.Connection);Blockly.RenderedConnection.TrackedState={WILL_TRACK:-1,UNTRACKED:0,TRACKED:1}; -Blockly.RenderedConnection.prototype.dispose=function(){Blockly.RenderedConnection.superClass_.dispose.call(this);this.trackedState_==Blockly.RenderedConnection.TrackedState.TRACKED&&this.db_.removeConnection(this,this.y)};Blockly.RenderedConnection.prototype.getSourceBlock=function(){return Blockly.RenderedConnection.superClass_.getSourceBlock.call(this)};Blockly.RenderedConnection.prototype.targetBlock=function(){return Blockly.RenderedConnection.superClass_.targetBlock.call(this)}; +e.left,b.clientY-e.top);var f=a.getOriginOffsetInPixels();e=Blockly.utils.Coordinate.difference(e,f);e.scale(1/a.scale);c.moveBy(e.x,e.y);a.rendered&&(c.initSvg(),c.render(),c.select())};return c};Blockly.RenderedConnection=function(a,b){Blockly.RenderedConnection.superClass_.constructor.call(this,a,b);this.db_=a.workspace.connectionDBList[b];this.dbOpposite_=a.workspace.connectionDBList[Blockly.OPPOSITE_TYPE[b]];this.offsetInBlock_=new Blockly.utils.Coordinate(0,0);this.trackedState_=Blockly.RenderedConnection.TrackedState.WILL_TRACK;this.targetConnection=null};Blockly.utils.object.inherits(Blockly.RenderedConnection,Blockly.Connection); +Blockly.RenderedConnection.TrackedState={WILL_TRACK:-1,UNTRACKED:0,TRACKED:1};Blockly.RenderedConnection.prototype.dispose=function(){Blockly.RenderedConnection.superClass_.dispose.call(this);this.trackedState_==Blockly.RenderedConnection.TrackedState.TRACKED&&this.db_.removeConnection(this,this.y)};Blockly.RenderedConnection.prototype.getSourceBlock=function(){return Blockly.RenderedConnection.superClass_.getSourceBlock.call(this)};Blockly.RenderedConnection.prototype.targetBlock=function(){return Blockly.RenderedConnection.superClass_.targetBlock.call(this)}; Blockly.RenderedConnection.prototype.distanceFrom=function(a){var b=this.x-a.x;a=this.y-a.y;return Math.sqrt(b*b+a*a)}; Blockly.RenderedConnection.prototype.bumpAwayFrom=function(a){if(!this.sourceBlock_.workspace.isDragging()){var b=this.sourceBlock_.getRootBlock();if(!b.isInFlyout){var c=!1;if(!b.isMovable()){b=a.getSourceBlock().getRootBlock();if(!b.isMovable())return;a=this;c=!0}var d=Blockly.selected==b;d||b.addSelect();var e=a.x+Blockly.SNAP_RADIUS+Math.floor(Math.random()*Blockly.BUMP_RANDOMNESS)-this.x,f=a.y+Blockly.SNAP_RADIUS+Math.floor(Math.random()*Blockly.BUMP_RANDOMNESS)-this.y;c&&(f=-f);b.RTL&&(e=a.x- Blockly.SNAP_RADIUS-Math.floor(Math.random()*Blockly.BUMP_RANDOMNESS)-this.x);b.moveBy(e,f);d||b.removeSelect()}}}; @@ -565,9 +577,10 @@ a));a=this.sourceBlock_.getRelativeToSurfaceXY();Blockly.Connection.highlightedP Blockly.RenderedConnection.prototype.setTracking=function(a){a&&this.trackedState_==Blockly.RenderedConnection.TrackedState.TRACKED||!a&&this.trackedState_==Blockly.RenderedConnection.TrackedState.UNTRACKED||this.sourceBlock_.isInFlyout||(a?(this.db_.addConnection(this,this.y),this.trackedState_=Blockly.RenderedConnection.TrackedState.TRACKED):(this.trackedState_==Blockly.RenderedConnection.TrackedState.TRACKED&&this.db_.removeConnection(this,this.y),this.trackedState_=Blockly.RenderedConnection.TrackedState.UNTRACKED))}; Blockly.RenderedConnection.prototype.stopTrackingAll=function(){this.setTracking(!1);if(this.targetConnection)for(var a=this.targetBlock().getDescendants(!1),b=0;bb?!1:Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this,a)};Blockly.RenderedConnection.prototype.onFailedConnect=function(a){this.bumpAwayFrom(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.isConnectionAllowed=function(a,b){return this.distanceFrom(a)>b?!1:Blockly.RenderedConnection.superClass_.isConnectionAllowed.call(this,a)};Blockly.RenderedConnection.prototype.onFailedConnect=function(a){this.bumpAwayFrom(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(),b.getSvgRoot().style.display="block")}; 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 Error("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.RenderedConnection.prototype.connect_=function(a){Blockly.RenderedConnection.superClass_.connect_.call(this,a);var b=this.getSourceBlock();a=a.getSourceBlock();var c=b.rendered,d=a.rendered;c&&b.updateDisabled();d&&a.updateDisabled();c&&d&&(this.type==Blockly.NEXT_STATEMENT||this.type==Blockly.PREVIOUS_STATEMENT?a.render():b.render());if(b=b.getInputWithBlock(a))b=b.isVisible(),a.getSvgRoot().style.display=b?"block":"none"}; Blockly.RenderedConnection.prototype.onCheckChanged_=function(){!this.isConnected()||this.targetConnection&&this.checkType(this.targetConnection)||((this.isSuperior()?this.targetBlock():this.sourceBlock_).unplug(),this.sourceBlock_.bumpNeighbours())};Blockly.Marker=function(){this.drawer_=this.curNode_=this.colour=null;this.type="marker"};Blockly.Marker.prototype.setDrawer=function(a){this.drawer_=a};Blockly.Marker.prototype.getDrawer=function(){return this.drawer_};Blockly.Marker.prototype.getCurNode=function(){return this.curNode_};Blockly.Marker.prototype.setCurNode=function(a){var b=this.curNode_;this.curNode_=a;this.drawer_&&this.drawer_.draw(b,this.curNode_)}; Blockly.Marker.prototype.draw=function(){this.drawer_&&this.drawer_.draw(this.curNode_,this.curNode_)};Blockly.Marker.prototype.hide=function(){this.drawer_&&this.drawer_.hide()};Blockly.Marker.prototype.dispose=function(){this.getDrawer()&&this.getDrawer().dispose()};Blockly.Cursor=function(){Blockly.Cursor.superClass_.constructor.call(this);this.type="cursor"};Blockly.utils.object.inherits(Blockly.Cursor,Blockly.Marker);Blockly.Cursor.prototype.next=function(){var a=this.getCurNode();if(!a)return null;for(a=a.next();a&&a.next()&&(a.getType()==Blockly.ASTNode.types.NEXT||a.getType()==Blockly.ASTNode.types.BLOCK);)a=a.next();a&&this.setCurNode(a);return a}; Blockly.Cursor.prototype.in=function(){var a=this.getCurNode();if(!a)return null;if(a.getType()==Blockly.ASTNode.types.PREVIOUS||a.getType()==Blockly.ASTNode.types.OUTPUT)a=a.next();(a=a.in())&&this.setCurNode(a);return a};Blockly.Cursor.prototype.prev=function(){var a=this.getCurNode();if(!a)return null;for(a=a.prev();a&&a.prev()&&(a.getType()==Blockly.ASTNode.types.NEXT||a.getType()==Blockly.ASTNode.types.BLOCK);)a=a.prev();a&&this.setCurNode(a);return a}; @@ -575,8 +588,8 @@ Blockly.Cursor.prototype.out=function(){var a=this.getCurNode();if(!a)return nul Blockly.Cursor.prototype.onBlocklyAction=function(a){if(this.getCurNode()&&this.getCurNode().getType()===Blockly.ASTNode.types.FIELD&&this.getCurNode().getLocation().onBlocklyAction(a))return!0;switch(a.name){case Blockly.navigation.actionNames.PREVIOUS:return this.prev(),!0;case Blockly.navigation.actionNames.OUT:return this.out(),!0;case Blockly.navigation.actionNames.NEXT:return this.next(),!0;case Blockly.navigation.actionNames.IN:return this.in(),!0;default:return!1}};Blockly.BasicCursor=function(){Blockly.BasicCursor.superClass_.constructor.call(this)};Blockly.utils.object.inherits(Blockly.BasicCursor,Blockly.Cursor);Blockly.BasicCursor.prototype.next=function(){var a=this.getCurNode();if(!a)return null;(a=this.getNextNode_(a,this.validNode_))&&this.setCurNode(a);return a};Blockly.BasicCursor.prototype.in=function(){return this.next()}; Blockly.BasicCursor.prototype.prev=function(){var a=this.getCurNode();if(!a)return null;(a=this.getPreviousNode_(a,this.validNode_))&&this.setCurNode(a);return a};Blockly.BasicCursor.prototype.out=function(){return this.prev()};Blockly.BasicCursor.prototype.getNextNode_=function(a,b){if(!a)return null;var c=a.in()||a.next();if(b(c))return c;if(c)return this.getNextNode_(c,b);a=this.findSiblingOrParent_(a.out());return b(a)?a:a?this.getNextNode_(a,b):null}; Blockly.BasicCursor.prototype.getPreviousNode_=function(a,b){if(!a)return null;var c=a.prev();c=c?this.getRightMostChild_(c):a.out();return b(c)?c:c?this.getPreviousNode_(c,b):null};Blockly.BasicCursor.prototype.validNode_=function(a){var b=!1;a=a&&a.getType();if(a==Blockly.ASTNode.types.OUTPUT||a==Blockly.ASTNode.types.INPUT||a==Blockly.ASTNode.types.FIELD||a==Blockly.ASTNode.types.NEXT||a==Blockly.ASTNode.types.PREVIOUS||a==Blockly.ASTNode.types.WORKSPACE)b=!0;return b}; -Blockly.BasicCursor.prototype.findSiblingOrParent_=function(a){if(!a)return null;var b=a.next();return b?b:this.findSiblingOrParent_(a.out())};Blockly.BasicCursor.prototype.getRightMostChild_=function(a){if(!a.in())return a;for(a=a.in();a.next();)a=a.next();return this.getRightMostChild_(a)};Blockly.TabNavigateCursor=function(){Blockly.TabNavigateCursor.superClass_.constructor.call(this)};Blockly.utils.object.inherits(Blockly.TabNavigateCursor,Blockly.BasicCursor);Blockly.TabNavigateCursor.prototype.validNode_=function(a){var b=!1,c=a&&a.getType();a&&(a=a.getLocation(),c==Blockly.ASTNode.types.FIELD&&a&&a.isTabNavigable()&&a.isClickable()&&(b=!0));return b};Blockly.utils.Rect=function(a,b,c,d){this.top=a;this.bottom=b;this.left=c;this.right=d};Blockly.utils.Rect.prototype.contains=function(a,b){return a>=this.left&&a<=this.right&&b>=this.top&&b<=this.bottom};Blockly.BlockSvg=function(a,b,c){this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{},null);this.svgGroup_.translate_="";this.style=a.getRenderer().getConstants().getBlockStyle(null);this.pathObject=a.getRenderer().makePathObject(this.svgGroup_,this.style);this.rendered=!1;this.workspace=a;this.previousConnection=this.nextConnection=this.outputConnection=null;this.useDragSurface_=Blockly.utils.is3dSupported()&&!!a.getBlockDragSurface();var d=this.pathObject.svgPath;d.tooltip=this;Blockly.Tooltip.bindMouseEvents(d); -Blockly.BlockSvg.superClass_.constructor.call(this,a,b,c);this.svgGroup_.dataset&&(this.svgGroup_.dataset.id=this.id)};Blockly.utils.object.inherits(Blockly.BlockSvg,Blockly.Block);Blockly.BlockSvg.prototype.height=0;Blockly.BlockSvg.prototype.width=0;Blockly.BlockSvg.prototype.dragStartXY_=null;Blockly.BlockSvg.prototype.warningTextDb_=null;Blockly.BlockSvg.INLINE=-1;Blockly.BlockSvg.COLLAPSED_WARNING_ID="TEMP_COLLAPSED_WARNING_"; +Blockly.BasicCursor.prototype.findSiblingOrParent_=function(a){if(!a)return null;var b=a.next();return b?b:this.findSiblingOrParent_(a.out())};Blockly.BasicCursor.prototype.getRightMostChild_=function(a){if(!a.in())return a;for(a=a.in();a.next();)a=a.next();return this.getRightMostChild_(a)};Blockly.TabNavigateCursor=function(){Blockly.TabNavigateCursor.superClass_.constructor.call(this)};Blockly.utils.object.inherits(Blockly.TabNavigateCursor,Blockly.BasicCursor);Blockly.TabNavigateCursor.prototype.validNode_=function(a){var b=!1,c=a&&a.getType();a&&(a=a.getLocation(),c==Blockly.ASTNode.types.FIELD&&a&&a.isTabNavigable()&&a.isClickable()&&(b=!0));return b};Blockly.BlockSvg=function(a,b,c){this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{},null);this.svgGroup_.translate_="";this.style=a.getRenderer().getConstants().getBlockStyle(null);this.pathObject=a.getRenderer().makePathObject(this.svgGroup_,this.style);this.renderIsInProgress_=this.rendered=!1;this.workspace=a;this.previousConnection=this.nextConnection=this.outputConnection=null;this.useDragSurface_=Blockly.utils.is3dSupported()&&!!a.getBlockDragSurface();var d=this.pathObject.svgPath;d.tooltip= +this;Blockly.Tooltip.bindMouseEvents(d);Blockly.BlockSvg.superClass_.constructor.call(this,a,b,c);this.svgGroup_.dataset&&(this.svgGroup_.dataset.id=this.id)};Blockly.utils.object.inherits(Blockly.BlockSvg,Blockly.Block);Blockly.BlockSvg.prototype.height=0;Blockly.BlockSvg.prototype.width=0;Blockly.BlockSvg.prototype.warningTextDb_=null;Blockly.BlockSvg.INLINE=-1;Blockly.BlockSvg.COLLAPSED_WARNING_ID="TEMP_COLLAPSED_WARNING_"; Blockly.BlockSvg.prototype.initSvg=function(){if(!this.workspace.rendered)throw TypeError("Workspace is headless.");for(var a=0,b;b=this.inputList[a];a++)b.init();b=this.getIcons();for(a=0;a=this.connections_.length)return-1;b=a.y;for(var d=c;0<=d&&this.connections_[d].y==b;){if(this.connections_[d]==a)return d;d--}for(;ca)c=d;else{b=d;break}}return b};Blockly.ConnectionDB.prototype.removeConnection=function(a,b){a=this.findIndexOfConnection_(a,b);if(-1==a)throw Error("Unable to find connection in connectionDB.");this.connections_.splice(a,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&&k.push(d[a]);return g=this.remainingCapacity()||(this.currentGesture_&&this.currentGesture_.cancel(),"comment"==a.tagName.toLowerCase()?this.pasteWorkspaceComment_(a):this.pasteBlock_(a))}; @@ -686,42 +700,44 @@ Blockly.WorkspaceSvg.prototype.pasteBlock_=function(a){Blockly.Events.disable(); if(1>=Math.abs(e-k.x)&&1>=Math.abs(f-k.y)){a=!0;break}}if(!a){var l=b.getConnections_(!1);c=0;for(var m;m=l[c];c++)if(m.closest(Blockly.SNAP_RADIUS,new Blockly.utils.Coordinate(e,f)).connection){a=!0;break}}a&&(e=this.RTL?e-Blockly.SNAP_RADIUS:e+Blockly.SNAP_RADIUS,f+=2*Blockly.SNAP_RADIUS)}while(a);b.moveBy(e,f)}}finally{Blockly.Events.enable()}Blockly.Events.isEnabled()&&!b.isShadow()&&Blockly.Events.fire(new Blockly.Events.BlockCreate(b));b.select()}; Blockly.WorkspaceSvg.prototype.pasteWorkspaceComment_=function(a){Blockly.Events.disable();try{var b=Blockly.WorkspaceCommentSvg.fromXml(a,this),c=parseInt(a.getAttribute("x"),10),d=parseInt(a.getAttribute("y"),10);isNaN(c)||isNaN(d)||(this.RTL&&(c=-c),b.moveBy(c+50,d+50))}finally{Blockly.Events.enable()}Blockly.Events.isEnabled();b.select()}; Blockly.WorkspaceSvg.prototype.refreshToolboxSelection=function(){var a=this.isFlyout?this.targetWorkspace:this;a&&!a.currentGesture_&&a.toolbox_&&a.toolbox_.getFlyout()&&a.toolbox_.refreshSelection()};Blockly.WorkspaceSvg.prototype.renameVariableById=function(a,b){Blockly.WorkspaceSvg.superClass_.renameVariableById.call(this,a,b);this.refreshToolboxSelection()};Blockly.WorkspaceSvg.prototype.deleteVariableById=function(a){Blockly.WorkspaceSvg.superClass_.deleteVariableById.call(this,a);this.refreshToolboxSelection()}; -Blockly.WorkspaceSvg.prototype.createVariable=function(a,b,c){a=Blockly.WorkspaceSvg.superClass_.createVariable.call(this,a,b,c);this.refreshToolboxSelection();return a};Blockly.WorkspaceSvg.prototype.recordDeleteAreas=function(){this.deleteAreaTrash_=this.trashcan&&this.svgGroup_.parentNode?this.trashcan.getClientRect():null;this.deleteAreaToolbox_=this.flyout_?this.flyout_.getClientRect():this.toolbox_?this.toolbox_.getClientRect():null}; +Blockly.WorkspaceSvg.prototype.createVariable=function(a,b,c){a=Blockly.WorkspaceSvg.superClass_.createVariable.call(this,a,b,c);this.refreshToolboxSelection();return a};Blockly.WorkspaceSvg.prototype.recordDeleteAreas=function(){this.deleteAreaTrash_=this.trashcan&&this.svgGroup_.parentNode?this.trashcan.getClientRect():null;this.deleteAreaToolbox_=this.flyout_?this.flyout_.getClientRect():this.toolbox_&&"function"==typeof this.toolbox_.getClientRect?this.toolbox_.getClientRect():null}; Blockly.WorkspaceSvg.prototype.isDeleteArea=function(a){return this.deleteAreaTrash_&&this.deleteAreaTrash_.contains(a.clientX,a.clientY)?Blockly.DELETE_AREA_TRASH:this.deleteAreaToolbox_&&this.deleteAreaToolbox_.contains(a.clientX,a.clientY)?Blockly.DELETE_AREA_TOOLBOX:Blockly.DELETE_AREA_NONE};Blockly.WorkspaceSvg.prototype.onMouseDown_=function(a){var b=this.getGesture(a);b&&b.handleWsStart(a,this)}; Blockly.WorkspaceSvg.prototype.startDrag=function(a,b){a=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM());a.x/=this.scale;a.y/=this.scale;this.dragDeltaXY_=Blockly.utils.Coordinate.difference(b,a)};Blockly.WorkspaceSvg.prototype.moveDrag=function(a){a=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM());a.x/=this.scale;a.y/=this.scale;return Blockly.utils.Coordinate.sum(this.dragDeltaXY_,a)}; Blockly.WorkspaceSvg.prototype.isDragging=function(){return null!=this.currentGesture_&&this.currentGesture_.isDragging()};Blockly.WorkspaceSvg.prototype.isDraggable=function(){return this.options.moveOptions&&this.options.moveOptions.drag}; Blockly.WorkspaceSvg.prototype.isContentBounded=function(){return this.options.moveOptions&&this.options.moveOptions.scrollbars||this.options.moveOptions&&this.options.moveOptions.wheel||this.options.moveOptions&&this.options.moveOptions.drag||this.options.zoomOptions&&this.options.zoomOptions.controls||this.options.zoomOptions&&this.options.zoomOptions.wheel||this.options.zoomOptions&&this.options.zoomOptions.pinch}; Blockly.WorkspaceSvg.prototype.isMovable=function(){return this.options.moveOptions&&this.options.moveOptions.scrollbars||this.options.moveOptions&&this.options.moveOptions.wheel||this.options.moveOptions&&this.options.moveOptions.drag||this.options.zoomOptions&&this.options.zoomOptions.wheel||this.options.zoomOptions&&this.options.zoomOptions.pinch}; Blockly.WorkspaceSvg.prototype.onMouseWheel_=function(a){if(Blockly.Gesture.inProgress())a.preventDefault(),a.stopPropagation();else{var b=this.options.zoomOptions&&this.options.zoomOptions.wheel,c=this.options.moveOptions&&this.options.moveOptions.wheel;if(b||c){var d=Blockly.utils.getScrollDeltaPixels(a);!b||!a.ctrlKey&&c?(b=this.scrollX-d.x,c=this.scrollY-d.y,a.shiftKey&&!d.x&&(b=this.scrollX-d.y,c=this.scrollY),this.scroll(b,c)):(d=-d.y/50,b=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM()), -this.zoom(b.x,b.y,d));a.preventDefault()}}};Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox=function(){var a=this.getTopBlocks(!1),b=this.getTopComments(!1);a=a.concat(b);if(!a.length)return new Blockly.utils.Rect(0,0,0,0);b=a[0].getBoundingRectangle();for(var c=1;cb.bottom&&(b.bottom=d.bottom);d.leftb.right&&(b.right=d.right)}return b}; +this.zoom(b.x,b.y,d));a.preventDefault()}}};Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox=function(){var a=this.getTopBoundedElements();if(!a.length)return new Blockly.utils.Rect(0,0,0,0);for(var b=a[0].getBoundingRectangle(),c=1;cb.bottom&&(b.bottom=d.bottom);d.leftb.right&&(b.right=d.right)}return b}; Blockly.WorkspaceSvg.prototype.cleanUp=function(){this.setResizesEnabled(!1);Blockly.Events.setGroup(!0);for(var a=this.getTopBlocks(!0),b=0,c=0,d;d=a[c];c++)if(d.isMovable()){var e=d.getRelativeToSurfaceXY();d.moveBy(-e.x,b-e.y);d.snapToGrid();b=d.getRelativeToSurfaceXY().y+d.getHeightWidth().height+this.renderer_.getConstants().MIN_BLOCK_HEIGHT}Blockly.Events.setGroup(!1);this.setResizesEnabled(!0)}; Blockly.WorkspaceSvg.prototype.showContextMenu=function(a){function b(a){if(a.isDeletable())p=p.concat(a.getDescendants(!1));else{a=a.getChildren(!1);for(var c=0;cp.length?c():Blockly.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace("%1",p.length),function(a){a&& c()})}};d.push(h);this.configureContextMenu&&this.configureContextMenu(d,a);Blockly.ContextMenu.show(a,d,this.RTL)}}; -Blockly.WorkspaceSvg.prototype.updateToolbox=function(a){if(a=Blockly.Options.parseToolboxTree(a)){if(!this.options.languageTree)throw Error("Existing toolbox is null. Can't create new toolbox.");if(a.getElementsByTagName("category").length){if(!this.toolbox_)throw Error("Existing toolbox has no categories. Can't change mode.");this.options.languageTree=a;this.toolbox_.renderTree(a)}else{if(!this.flyout_)throw Error("Existing toolbox has categories. Can't change mode.");this.options.languageTree= -a;this.flyout_.show(a.childNodes)}}else if(this.options.languageTree)throw Error("Can't nullify an existing toolbox.");};Blockly.WorkspaceSvg.prototype.markFocused=function(){this.options.parentWorkspace?this.options.parentWorkspace.markFocused():(Blockly.mainWorkspace=this,this.setBrowserFocus())};Blockly.WorkspaceSvg.prototype.setBrowserFocus=function(){document.activeElement&&document.activeElement.blur();try{this.getParentSvg().focus({preventScroll:!0})}catch(a){try{this.getParentSvg().parentNode.setActive()}catch(b){this.getParentSvg().parentNode.focus({preventScroll:!0})}}}; +Blockly.WorkspaceSvg.prototype.updateToolbox=function(a){Array.isArray(a)||(a=Blockly.Options.parseToolboxTree(a));if(a=Blockly.utils.toolbox.convertToolboxToJSON(a)){if(!this.options.languageTree)throw Error("Existing toolbox is null. Can't create new toolbox.");if(Blockly.utils.toolbox.hasCategories(a)){if(!this.toolbox_)throw Error("Existing toolbox has no categories. Can't change mode.");this.options.languageTree=a;this.toolbox_.render(a)}else{if(!this.flyout_)throw Error("Existing toolbox has categories. Can't change mode."); +this.options.languageTree=a;this.flyout_.show(a)}}else if(this.options.languageTree)throw Error("Can't nullify an existing toolbox.");};Blockly.WorkspaceSvg.prototype.markFocused=function(){this.options.parentWorkspace?this.options.parentWorkspace.markFocused():(Blockly.mainWorkspace=this,this.setBrowserFocus())};Blockly.WorkspaceSvg.prototype.setBrowserFocus=function(){document.activeElement&&document.activeElement.blur();try{this.getParentSvg().focus({preventScroll:!0})}catch(a){try{this.getParentSvg().parentNode.setActive()}catch(b){this.getParentSvg().parentNode.focus({preventScroll:!0})}}}; Blockly.WorkspaceSvg.prototype.zoom=function(a,b,c){c=Math.pow(this.options.zoomOptions.scaleSpeed,c);var d=this.scale*c;if(this.scale!=d){d>this.options.zoomOptions.maxScale?c=this.options.zoomOptions.maxScale/this.scale:dthis.options.zoomOptions.maxScale?a=this.options.zoomOptions.maxScale:this.options.zoomOptions.minScale&&ab.viewBottom||b.contentLeftb.viewRight){c=null;a&&(c=Blockly.Events.getGroup(),Blockly.Events.setGroup(a.group));switch(a.type){case Blockly.Events.BLOCK_CREATE:case Blockly.Events.BLOCK_MOVE:var f= e.getBlockById(a.blockId);f&&(f=f.getRootBlock());break;case Blockly.Events.COMMENT_CREATE:case Blockly.Events.COMMENT_MOVE:f=e.getCommentById(a.commentId)}if(f){d=f.getBoundingRectangle();d.height=d.bottom-d.top;d.width=d.right-d.left;var m=b.viewTop,n=b.viewBottom-d.height;n=Math.max(m,n);m=Blockly.utils.math.clamp(m,d.top,n)-d.top;n=b.viewLeft;var p=b.viewRight-d.width;b.RTL?n=Math.min(p,n):p=Math.max(n,p);b=Blockly.utils.math.clamp(n,d.left,p)-d.left;f.moveBy(b,m)}a&&(!a.group&&f&&console.log("WARNING: Moved object in bounds but there was no event group. This may break undo."), null!==c&&Blockly.Events.setGroup(c))}}});Blockly.svgResize(e);Blockly.WidgetDiv.createDom();Blockly.DropDownDiv.createDom();Blockly.Tooltip.createDom();return e}; -Blockly.init_=function(a){var b=a.options,c=a.getParentSvg();Blockly.bindEventWithChecks_(c.parentNode,"contextmenu",null,function(a){Blockly.utils.isTargetInput(a)||a.preventDefault()});c=Blockly.bindEventWithChecks_(window,"resize",null,function(){Blockly.hideChaff(!0);Blockly.svgResize(a)});a.setResizeHandlerWrapper(c);Blockly.inject.bindDocumentEvents_();if(b.languageTree){c=a.getToolbox();var d=a.getFlyout(!0);c?c.init():d&&(d.init(a),d.show(b.languageTree.childNodes),d.scrollToStart())}c=Blockly.Scrollbar.scrollbarThickness; +Blockly.init_=function(a){var b=a.options,c=a.getParentSvg();Blockly.bindEventWithChecks_(c.parentNode,"contextmenu",null,function(a){Blockly.utils.isTargetInput(a)||a.preventDefault()});c=Blockly.bindEventWithChecks_(window,"resize",null,function(){Blockly.hideChaff(!0);Blockly.svgResize(a)});a.setResizeHandlerWrapper(c);Blockly.inject.bindDocumentEvents_();if(b.languageTree){c=a.getToolbox();var d=a.getFlyout(!0);c?c.init():d&&(d.init(a),d.show(b.languageTree),d.scrollToStart())}c=Blockly.Scrollbar.scrollbarThickness; b.hasTrashcan&&(c=a.trashcan.init(c));b.zoomOptions&&b.zoomOptions.controls&&a.zoomControls_.init(c);b.moveOptions&&b.moveOptions.scrollbars?(a.scrollbar=new Blockly.ScrollbarPair(a),a.scrollbar.resize()):a.setMetrics({x:.5,y:.5});b.hasSounds&&Blockly.inject.loadSounds_(b.pathToMedia,a)}; Blockly.inject.bindDocumentEvents_=function(){Blockly.documentEventsBound_||(Blockly.bindEventWithChecks_(document,"scroll",null,function(){for(var a=Blockly.Workspace.getAll(),b=0,c;c=a[b];b++)c.updateInverseScreenCTM&&c.updateInverseScreenCTM()}),Blockly.bindEventWithChecks_(document,"keydown",null,Blockly.onKeyDown),Blockly.bindEvent_(document,"touchend",null,Blockly.longStop_),Blockly.bindEvent_(document,"touchcancel",null,Blockly.longStop_),Blockly.utils.userAgent.IPAD&&Blockly.bindEventWithChecks_(window, "orientationchange",document,function(){Blockly.svgResize(Blockly.getMainWorkspace())}));Blockly.documentEventsBound_=!0}; @@ -738,8 +754,8 @@ Blockly.inject.loadSounds_=function(a,b){var c=b.getAudioManager();c.load([a+"cl Blockly.Names.prototype.getNameForUserVariable_=function(a){return this.variableMap_?(a=this.variableMap_.getVariableById(a))?a.name:null:(console.log("Deprecated call to Blockly.Names.prototype.getName without defining a variable map. To fix, add the following code in your generator's init() function:\nBlockly.YourGeneratorName.variableDB_.setVariableMap(workspace.getVariableMap());"),null)}; Blockly.Names.prototype.getName=function(a,b){if(b==Blockly.VARIABLE_CATEGORY_NAME){var c=this.getNameForUserVariable_(a);c&&(a=c)}c=a.toLowerCase()+"_"+b;var d=b==Blockly.VARIABLE_CATEGORY_NAME||b==Blockly.Names.DEVELOPER_VARIABLE_TYPE?this.variablePrefix_:"";if(c in this.db_)return d+this.db_[c];a=this.getDistinctName(a,b);this.db_[c]=a.substr(d.length);return a}; Blockly.Names.prototype.getDistinctName=function(a,b){a=this.safeName_(a);for(var c="";this.dbReverse_[a+c]||a+c in this.reservedDict_;)c=c?c+1:2;a+=c;this.dbReverse_[a]=!0;return(b==Blockly.VARIABLE_CATEGORY_NAME||b==Blockly.Names.DEVELOPER_VARIABLE_TYPE?this.variablePrefix_:"")+a};Blockly.Names.prototype.safeName_=function(a){a?(a=encodeURI(a.replace(/ /g,"_")).replace(/[^\w]/g,"_"),-1!="0123456789".indexOf(a[0])&&(a="my_"+a)):a=Blockly.Msg.UNNAMED_KEY||"unnamed";return a}; -Blockly.Names.equals=function(a,b){return a.toLowerCase()==b.toLowerCase()};Blockly.Procedures={};Blockly.Procedures.NAME_TYPE=Blockly.PROCEDURE_CATEGORY_NAME;Blockly.Procedures.DEFAULT_ARG="x";Blockly.Procedures.allProcedures=function(a){a=a.getAllBlocks(!1);for(var b=[],c=[],d=0;d1'),d.appendChild(c),b.push(d));if(Blockly.Blocks.variables_get){a.sort(Blockly.VariableModel.compareByName);c=0;for(var e;e=a[c];c++)d=Blockly.utils.xml.createElement("block"),d.setAttribute("type","variables_get"),d.setAttribute("gap",8),d.appendChild(Blockly.Variables.generateVariableFieldDom(e)),b.push(d)}}return b}; Blockly.Variables.VAR_LETTER_OPTIONS="ijkmnopqrstuvwxyzabcdefgh";Blockly.Variables.generateUniqueName=function(a){return Blockly.Variables.generateUniqueNameFromOptions(Blockly.Variables.VAR_LETTER_OPTIONS.charAt(0),a.getAllVariableNames())}; Blockly.Variables.generateUniqueNameFromOptions=function(a,b){if(!b.length)return a;for(var c=Blockly.Variables.VAR_LETTER_OPTIONS,d="",e=c.indexOf(a);;){for(var f=!1,g=0;ge?Blockly.WidgetDiv.positionInternal_(a,0,c.height+e):Blockly.WidgetDiv.positionInternal_(a,e,c.height)};Blockly.WidgetDiv.calculateX_=function(a,b,c,d){if(d)return b=Math.max(b.right-c.width,a.left),Math.min(b,a.right-c.width);b=Math.min(b.left,a.right-c.width);return Math.max(b,a.left)}; -Blockly.WidgetDiv.calculateY_=function(a,b,c){return b.bottom+c.height>=a.bottom?b.top-c.height:b.bottom};Blockly.VERSION="3.20200402.1";Blockly.mainWorkspace=null;Blockly.selected=null;Blockly.draggingConnections=[];Blockly.clipboardXml_=null;Blockly.clipboardSource_=null;Blockly.clipboardTypeCounts_=null;Blockly.cache3dSupported_=null;Blockly.parentContainer=null;Blockly.svgSize=function(a){return{width:a.cachedWidth_,height:a.cachedHeight_}};Blockly.resizeSvgContents=function(a){a.resizeContents()}; +Blockly.WidgetDiv.calculateY_=function(a,b,c){return b.bottom+c.height>=a.bottom?b.top-c.height:b.bottom};Blockly.VERSION="3.20200625.0";Blockly.mainWorkspace=null;Blockly.selected=null;Blockly.draggingConnections=[];Blockly.clipboardXml_=null;Blockly.clipboardSource_=null;Blockly.clipboardTypeCounts_=null;Blockly.cache3dSupported_=null;Blockly.parentContainer=null;Blockly.svgSize=function(a){return new Blockly.utils.Size(a.cachedWidth_,a.cachedHeight_)};Blockly.resizeSvgContents=function(a){a.resizeContents()}; Blockly.svgResize=function(a){for(;a.options.parentWorkspace;)a=a.options.parentWorkspace;var b=a.getParentSvg(),c=b.parentNode;if(c){var d=c.offsetWidth;c=c.offsetHeight;b.cachedWidth_!=d&&(b.setAttribute("width",d+"px"),b.cachedWidth_=d);b.cachedHeight_!=c&&(b.setAttribute("height",c+"px"),b.cachedHeight_=c);a.resize()}}; -Blockly.onKeyDown=function(a){var b=Blockly.mainWorkspace;if(b&&!(Blockly.utils.isTargetInput(a)||b.rendered&&!b.isVisible()))if(b.options.readOnly)Blockly.navigation.onKeyPress(a);else{var c=!1;if(a.keyCode==Blockly.utils.KeyCodes.ESC)Blockly.hideChaff(),Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_EXIT);else{if(Blockly.navigation.onKeyPress(a))return;if(a.keyCode==Blockly.utils.KeyCodes.BACKSPACE||a.keyCode==Blockly.utils.KeyCodes.DELETE){a.preventDefault();if(Blockly.Gesture.inProgress())return; -Blockly.selected&&Blockly.selected.isDeletable()&&(c=!0)}else if(a.altKey||a.ctrlKey||a.metaKey){if(Blockly.Gesture.inProgress())return;Blockly.selected&&Blockly.selected.isDeletable()&&Blockly.selected.isMovable()&&(a.keyCode==Blockly.utils.KeyCodes.C?(Blockly.hideChaff(),Blockly.copy_(Blockly.selected)):a.keyCode!=Blockly.utils.KeyCodes.X||Blockly.selected.workspace.isFlyout||(Blockly.copy_(Blockly.selected),c=!0));a.keyCode==Blockly.utils.KeyCodes.V?Blockly.clipboardXml_&&(a=Blockly.clipboardSource_, -a.isFlyout&&(a=a.targetWorkspace),Blockly.clipboardTypeCounts_&&a.isCapacityAvailable(Blockly.clipboardTypeCounts_)&&(Blockly.Events.setGroup(!0),a.paste(Blockly.clipboardXml_),Blockly.Events.setGroup(!1))):a.keyCode==Blockly.utils.KeyCodes.Z&&(Blockly.hideChaff(),b.undo(a.shiftKey))}}c&&!Blockly.selected.workspace.isFlyout&&(Blockly.Events.setGroup(!0),Blockly.hideChaff(),Blockly.selected.dispose(!0,!0),Blockly.Events.setGroup(!1))}}; -Blockly.copy_=function(a){if(a.isComment)var b=a.toXmlWithXY();else{b=Blockly.Xml.blockToDom(a,!0);Blockly.Xml.deleteNext(b);var c=a.getRelativeToSurfaceXY();b.setAttribute("x",a.RTL?-c.x:c.x);b.setAttribute("y",c.y)}Blockly.clipboardXml_=b;Blockly.clipboardSource_=a.workspace;Blockly.clipboardTypeCounts_=a.isComment?null:Blockly.utils.getBlockTypeCounts(a,!0)}; -Blockly.duplicate=function(a){var b=Blockly.clipboardXml_,c=Blockly.clipboardSource_;Blockly.copy_(a);a.workspace.paste(Blockly.clipboardXml_);Blockly.clipboardXml_=b;Blockly.clipboardSource_=c};Blockly.onContextMenu_=function(a){Blockly.utils.isTargetInput(a)||a.preventDefault()}; +Blockly.onKeyDown=function(a){var b=Blockly.mainWorkspace;if(b&&!(Blockly.utils.isTargetInput(a)||b.rendered&&!b.isVisible()))if(b.options.readOnly)Blockly.navigation.onKeyPress(a);else{var c=!1;if(a.keyCode==Blockly.utils.KeyCodes.ESC)Blockly.hideChaff(),Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_EXIT);else{if(!Blockly.Gesture.inProgress()&&Blockly.navigation.onKeyPress(a))return;if(a.keyCode==Blockly.utils.KeyCodes.BACKSPACE||a.keyCode==Blockly.utils.KeyCodes.DELETE){a.preventDefault(); +if(Blockly.Gesture.inProgress())return;Blockly.selected&&Blockly.selected.isDeletable()&&(c=!0)}else if(a.altKey||a.ctrlKey||a.metaKey){if(Blockly.Gesture.inProgress())return;Blockly.selected&&Blockly.selected.isDeletable()&&Blockly.selected.isMovable()&&(a.keyCode==Blockly.utils.KeyCodes.C?(Blockly.hideChaff(),Blockly.copy_(Blockly.selected)):a.keyCode!=Blockly.utils.KeyCodes.X||Blockly.selected.workspace.isFlyout||(Blockly.copy_(Blockly.selected),c=!0));a.keyCode==Blockly.utils.KeyCodes.V?Blockly.clipboardXml_&& +(a=Blockly.clipboardSource_,a.isFlyout&&(a=a.targetWorkspace),Blockly.clipboardTypeCounts_&&a.isCapacityAvailable(Blockly.clipboardTypeCounts_)&&(Blockly.Events.setGroup(!0),a.paste(Blockly.clipboardXml_),Blockly.Events.setGroup(!1))):a.keyCode==Blockly.utils.KeyCodes.Z?(Blockly.hideChaff(),b.undo(a.shiftKey)):a.ctrlKey&&a.keyCode==Blockly.utils.KeyCodes.Y&&(Blockly.hideChaff(),b.undo(!0))}}c&&!Blockly.selected.workspace.isFlyout&&(Blockly.Events.setGroup(!0),Blockly.hideChaff(),Blockly.selected.dispose(!0, +!0),Blockly.Events.setGroup(!1))}};Blockly.copy_=function(a){a=a.toCopyData();Blockly.clipboardXml_=a.xml;Blockly.clipboardSource_=a.source;Blockly.clipboardTypeCounts_=a.typeCounts};Blockly.duplicate=function(a){var b=Blockly.clipboardXml_,c=Blockly.clipboardSource_;Blockly.copy_(a);a.workspace.paste(Blockly.clipboardXml_);Blockly.clipboardXml_=b;Blockly.clipboardSource_=c};Blockly.onContextMenu_=function(a){Blockly.utils.isTargetInput(a)||a.preventDefault()}; Blockly.hideChaff=function(a){Blockly.Tooltip.hide();Blockly.WidgetDiv.hide();Blockly.DropDownDiv.hideWithoutAnimation();a||(a=Blockly.getMainWorkspace(),a.trashcan&&a.trashcan.flyout&&a.trashcan.flyout.hide(),(a=a.getToolbox())&&a.getFlyout()&&a.getFlyout().autoClose&&a.clearSelection())};Blockly.getMainWorkspace=function(){return Blockly.mainWorkspace};Blockly.alert=function(a,b){alert(a);b&&b()};Blockly.confirm=function(a,b){b(confirm(a))};Blockly.prompt=function(a,b,c){c(prompt(a,b))}; Blockly.jsonInitFactory_=function(a){return function(){this.jsonInit(a)}}; Blockly.defineBlocksWithJsonArray=function(a){for(var b=0;b90-b||a>-90-b&&a<-90+b?!0:!1}; Blockly.HorizontalFlyout.prototype.getClientRect=function(){if(!this.svgGroup_)return null;var a=this.svgGroup_.getBoundingClientRect(),b=a.top;return this.toolboxPosition_==Blockly.TOOLBOX_AT_TOP?new Blockly.utils.Rect(-1E9,b+a.height,-1E9,1E9):new Blockly.utils.Rect(b,1E9,-1E9,1E9)}; -Blockly.HorizontalFlyout.prototype.reflowInternal_=function(){this.workspace_.scale=this.targetWorkspace_.scale;for(var a=0,b=this.workspace_.getTopBlocks(!1),c=0,d;d=b[c];c++)a=Math.max(a,d.getHeightWidth().height);a+=1.5*this.MARGIN;a*=this.workspace_.scale;a+=Blockly.Scrollbar.scrollbarThickness;if(this.height_!=a){for(c=0;d=b[c];c++)d.flyoutRect_&&this.moveRectToBlock_(d.flyoutRect_,d);this.height_=a;this.position()}};Blockly.VerticalFlyout=function(a){a.getMetrics=this.getMetrics_.bind(this);a.setMetrics=this.setMetrics_.bind(this);Blockly.VerticalFlyout.superClass_.constructor.call(this,a);this.horizontalLayout_=!1};Blockly.utils.object.inherits(Blockly.VerticalFlyout,Blockly.Flyout); -Blockly.VerticalFlyout.prototype.getMetrics_=function(){if(!this.isVisible())return null;try{var a=this.workspace_.getCanvas().getBBox()}catch(e){a={height:0,y:0,width:0,x:0}}var b=this.SCROLLBAR_PADDING,c=this.height_-2*this.SCROLLBAR_PADDING,d=this.width_;this.RTL||(d-=this.SCROLLBAR_PADDING);return{viewHeight:c,viewWidth:d,contentHeight:a.height*this.workspace_.scale+2*this.MARGIN,contentWidth:a.width*this.workspace_.scale+2*this.MARGIN,viewTop:-this.workspace_.scrollY+a.y,viewLeft:-this.workspace_.scrollX, -contentTop:a.y,contentLeft:a.x,absoluteTop:b,absoluteLeft:0}};Blockly.VerticalFlyout.prototype.setMetrics_=function(a){var b=this.getMetrics_();b&&("number"==typeof a.y&&(this.workspace_.scrollY=-b.contentHeight*a.y),this.workspace_.translate(this.workspace_.scrollX+b.absoluteLeft,this.workspace_.scrollY+b.absoluteTop))}; -Blockly.VerticalFlyout.prototype.position=function(){if(this.isVisible()){var a=this.targetWorkspace_.getMetrics();a&&(this.height_=a.viewHeight,this.setBackgroundPath_(this.width_-this.CORNER_RADIUS,a.viewHeight-2*this.CORNER_RADIUS),this.positionAt_(this.width_,this.height_,this.targetWorkspace_.toolboxPosition==this.toolboxPosition_?a.toolboxWidth?this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?a.toolboxWidth:a.viewWidth-this.width_:this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?0:a.viewWidth:this.toolboxPosition_== +Blockly.HorizontalFlyout.prototype.reflowInternal_=function(){this.workspace_.scale=this.targetWorkspace.scale;for(var a=0,b=this.workspace_.getTopBlocks(!1),c=0,d;d=b[c];c++)a=Math.max(a,d.getHeightWidth().height);a+=1.5*this.MARGIN;a*=this.workspace_.scale;a+=Blockly.Scrollbar.scrollbarThickness;if(this.height_!=a){for(c=0;d=b[c];c++)d.flyoutRect_&&this.moveRectToBlock_(d.flyoutRect_,d);this.height_=a;this.position()}};Blockly.VerticalFlyout=function(a){Blockly.VerticalFlyout.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.VerticalFlyout,Blockly.Flyout); +Blockly.VerticalFlyout.prototype.getMetrics_=function(){if(!this.isVisible())return null;try{var a=this.workspace_.getCanvas().getBBox()}catch(e){a={height:0,y:0,width:0,x:0}}var b=this.SCROLLBAR_PADDING,c=this.height_-2*this.SCROLLBAR_PADDING,d=this.width_;this.RTL||(d-=this.SCROLLBAR_PADDING);return{contentHeight:a.height*this.workspace_.scale+2*this.MARGIN,contentWidth:a.width*this.workspace_.scale+2*this.MARGIN,contentTop:a.y,contentLeft:a.x,viewHeight:c,viewWidth:d,viewTop:-this.workspace_.scrollY+ +a.y,viewLeft:-this.workspace_.scrollX,absoluteTop:b,absoluteLeft:0}};Blockly.VerticalFlyout.prototype.setMetrics_=function(a){var b=this.getMetrics_();b&&("number"==typeof a.y&&(this.workspace_.scrollY=-b.contentHeight*a.y),this.workspace_.translate(this.workspace_.scrollX+b.absoluteLeft,this.workspace_.scrollY+b.absoluteTop))}; +Blockly.VerticalFlyout.prototype.position=function(){if(this.isVisible()){var a=this.targetWorkspace.getMetrics();a&&(this.height_=a.viewHeight,this.setBackgroundPath_(this.width_-this.CORNER_RADIUS,a.viewHeight-2*this.CORNER_RADIUS),this.positionAt_(this.width_,this.height_,this.targetWorkspace.toolboxPosition==this.toolboxPosition_?a.toolboxWidth?this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?a.toolboxWidth:a.viewWidth-this.width_:this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?0:a.viewWidth:this.toolboxPosition_== Blockly.TOOLBOX_AT_LEFT?0:a.viewWidth+a.absoluteLeft-this.width_,0))}}; Blockly.VerticalFlyout.prototype.setBackgroundPath_=function(a,b){var c=this.toolboxPosition_==Blockly.TOOLBOX_AT_RIGHT,d=a+this.CORNER_RADIUS;d=["M "+(c?d:0)+",0"];d.push("h",c?-a:a);d.push("a",this.CORNER_RADIUS,this.CORNER_RADIUS,0,0,c?0:1,c?-this.CORNER_RADIUS:this.CORNER_RADIUS,this.CORNER_RADIUS);d.push("v",Math.max(0,b));d.push("a",this.CORNER_RADIUS,this.CORNER_RADIUS,0,0,c?0:1,c?this.CORNER_RADIUS:-this.CORNER_RADIUS,this.CORNER_RADIUS);d.push("h",c?a:-a);d.push("z");this.svgBackground_.setAttribute("d", -d.join(" "))};Blockly.VerticalFlyout.prototype.scrollToStart=function(){this.scrollbar_.set(0)};Blockly.VerticalFlyout.prototype.wheel_=function(a){var b=Blockly.utils.getScrollDeltaPixels(a);if(b.y){var c=this.getMetrics_();b=c.viewTop-c.contentTop+b.y;b=Math.min(b,c.contentHeight-c.viewHeight);b=Math.max(b,0);this.scrollbar_.set(b);Blockly.WidgetDiv.hide()}a.preventDefault();a.stopPropagation()}; -Blockly.VerticalFlyout.prototype.layout_=function(a,b){this.workspace_.scale=this.targetWorkspace_.scale;for(var c=this.MARGIN,d=this.RTL?c:c+this.tabWidth_,e=0,f;f=a[e];e++)if("block"==f.type){f=f.block;for(var g=f.getDescendants(!1),h=0,k;k=g[h];h++)k.isInFlyout=!0;f.render();g=f.getSvgRoot();h=f.getHeightWidth();k=f.outputConnection?d-this.tabWidth_:d;f.moveBy(k,c);k=this.createRect_(f,this.RTL?k-h.width:k,c,h,e);this.addBlockListeners_(g,f,k);c+=h.height+b[e]}else"button"==f.type&&(this.initFlyoutButton_(f.button, +d.join(" "))};Blockly.VerticalFlyout.prototype.scrollToStart=function(){this.scrollbar.set(0)};Blockly.VerticalFlyout.prototype.wheel_=function(a){var b=Blockly.utils.getScrollDeltaPixels(a);if(b.y){var c=this.getMetrics_();b=c.viewTop-c.contentTop+b.y;b=Math.min(b,c.contentHeight-c.viewHeight);b=Math.max(b,0);this.scrollbar.set(b);Blockly.WidgetDiv.hide();Blockly.DropDownDiv.hideWithoutAnimation()}a.preventDefault();a.stopPropagation()}; +Blockly.VerticalFlyout.prototype.layout_=function(a,b){this.workspace_.scale=this.targetWorkspace.scale;for(var c=this.MARGIN,d=this.RTL?c:c+this.tabWidth_,e=0,f;f=a[e];e++)if("block"==f.type){f=f.block;for(var g=f.getDescendants(!1),h=0,k;k=g[h];h++)k.isInFlyout=!0;f.render();g=f.getSvgRoot();h=f.getHeightWidth();k=f.outputConnection?d-this.tabWidth_:d;f.moveBy(k,c);k=this.createRect_(f,this.RTL?k-h.width:k,c,h,e);this.addBlockListeners_(g,f,k);c+=h.height+b[e]}else"button"==f.type&&(this.initFlyoutButton_(f.button, d,c),c+=f.button.height+b[e])};Blockly.VerticalFlyout.prototype.isDragTowardWorkspace=function(a){a=Math.atan2(a.y,a.x)/Math.PI*180;var b=this.dragAngleRange_;return a-b||a<-180+b||a>180-b?!0:!1};Blockly.VerticalFlyout.prototype.getClientRect=function(){if(!this.svgGroup_)return null;var a=this.svgGroup_.getBoundingClientRect(),b=a.left;return this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT?new Blockly.utils.Rect(-1E9,1E9,-1E9,b+a.width):new Blockly.utils.Rect(-1E9,1E9,b,1E9)}; -Blockly.VerticalFlyout.prototype.reflowInternal_=function(){this.workspace_.scale=this.targetWorkspace_.scale;for(var a=0,b=this.workspace_.getTopBlocks(!1),c=0,d;d=b[c];c++){var e=d.getHeightWidth().width;d.outputConnection&&(e-=this.tabWidth_);a=Math.max(a,e)}for(c=0;d=this.buttons_[c];c++)a=Math.max(a,d.width);a+=1.5*this.MARGIN+this.tabWidth_;a*=this.workspace_.scale;a+=Blockly.Scrollbar.scrollbarThickness;if(this.width_!=a){for(c=0;d=b[c];c++){if(this.RTL){e=d.getRelativeToSurfaceXY().x;var f= -a/this.workspace_.scale-this.MARGIN;d.outputConnection||(f-=this.tabWidth_);d.moveBy(f-e,0)}d.flyoutRect_&&this.moveRectToBlock_(d.flyoutRect_,d)}if(this.RTL)for(c=0;d=this.buttons_[c];c++)b=d.getPosition().y,d.moveTo(a/this.workspace_.scale-d.width-this.MARGIN-this.tabWidth_,b);this.width_=a;this.position()}};Blockly.FlyoutButton=function(a,b,c,d){this.workspace_=a;this.targetWorkspace_=b;this.text_=c.getAttribute("text");this.position_=new Blockly.utils.Coordinate(0,0);this.isLabel_=d;this.callbackKey_=c.getAttribute("callbackKey")||c.getAttribute("callbackkey");this.cssClass_=c.getAttribute("web-class")||null;this.onMouseUpWrapper_=null};Blockly.FlyoutButton.MARGIN_X=5;Blockly.FlyoutButton.MARGIN_Y=2;Blockly.FlyoutButton.prototype.width=0;Blockly.FlyoutButton.prototype.height=0; +Blockly.VerticalFlyout.prototype.reflowInternal_=function(){this.workspace_.scale=this.targetWorkspace.scale;for(var a=0,b=this.workspace_.getTopBlocks(!1),c=0,d;d=b[c];c++){var e=d.getHeightWidth().width;d.outputConnection&&(e-=this.tabWidth_);a=Math.max(a,e)}for(c=0;d=this.buttons_[c];c++)a=Math.max(a,d.width);a+=1.5*this.MARGIN+this.tabWidth_;a*=this.workspace_.scale;a+=Blockly.Scrollbar.scrollbarThickness;if(this.width_!=a){for(c=0;d=b[c];c++){if(this.RTL){e=d.getRelativeToSurfaceXY().x;var f= +a/this.workspace_.scale-this.MARGIN;d.outputConnection||(f-=this.tabWidth_);d.moveBy(f-e,0)}d.flyoutRect_&&this.moveRectToBlock_(d.flyoutRect_,d)}if(this.RTL)for(c=0;d=this.buttons_[c];c++)b=d.getPosition().y,d.moveTo(a/this.workspace_.scale-d.width-this.MARGIN-this.tabWidth_,b);this.width_=a;this.position()}};Blockly.FlyoutButton=function(a,b,c,d){this.workspace_=a;this.targetWorkspace_=b;this.text_=c.text;this.position_=new Blockly.utils.Coordinate(0,0);this.isLabel_=d;this.callbackKey_=c.callbackKey||c.callbackkey;this.cssClass_=c["web-class"]||null;this.onMouseUpWrapper_=null};Blockly.FlyoutButton.MARGIN_X=5;Blockly.FlyoutButton.MARGIN_Y=2;Blockly.FlyoutButton.prototype.width=0;Blockly.FlyoutButton.prototype.height=0; Blockly.FlyoutButton.prototype.createDom=function(){var a=this.isLabel_?"blocklyFlyoutLabel":"blocklyFlyoutButton";this.cssClass_&&(a+=" "+this.cssClass_);this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{"class":a},this.workspace_.getCanvas());if(!this.isLabel_)var b=Blockly.utils.dom.createSvgElement("rect",{"class":"blocklyFlyoutButtonShadow",rx:4,ry:4,x:1,y:1},this.svgGroup_);a=Blockly.utils.dom.createSvgElement("rect",{"class":this.isLabel_?"blocklyFlyoutLabelBackground":"blocklyFlyoutButtonBackground", rx:4,ry:4},this.svgGroup_);var c=Blockly.utils.dom.createSvgElement("text",{"class":this.isLabel_?"blocklyFlyoutLabelText":"blocklyText",x:0,y:0,"text-anchor":"middle"},this.svgGroup_),d=Blockly.utils.replaceMessageReferences(this.text_);this.workspace_.RTL&&(d+="\u200f");c.textContent=d;this.isLabel_&&(this.svgText_=c,this.workspace_.getThemeManager().subscribe(this.svgText_,"flyoutForegroundColour","fill"));var e=Blockly.utils.style.getComputedStyle(c,"fontSize"),f=Blockly.utils.style.getComputedStyle(c, "fontWeight"),g=Blockly.utils.style.getComputedStyle(c,"fontFamily");this.width=Blockly.utils.dom.getFastTextWidthWithSizeString(c,e,f,g);d=Blockly.utils.dom.measureFontMetrics(d,e,f,g);this.height=d.height;this.isLabel_||(this.width+=2*Blockly.FlyoutButton.MARGIN_X,this.height+=2*Blockly.FlyoutButton.MARGIN_Y,b.setAttribute("width",this.width),b.setAttribute("height",this.height));a.setAttribute("width",this.width);a.setAttribute("height",this.height);c.setAttribute("x",this.width/2);c.setAttribute("y", @@ -860,75 +877,75 @@ Blockly.FlyoutButton.prototype.moveTo=function(a,b){this.position_.x=a;this.posi Blockly.FlyoutButton.prototype.onMouseUp_=function(a){(a=this.targetWorkspace_.getGesture(a))&&a.cancel();this.isLabel_&&this.callbackKey_?console.warn("Labels should not have callbacks. Label text: "+this.text_):this.isLabel_||this.callbackKey_&&this.targetWorkspace_.getButtonCallback(this.callbackKey_)?this.isLabel_||this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this):console.warn("Buttons should have callbacks. Button text: "+this.text_)};Blockly.Css.register(".blocklyFlyoutButton {,fill: #888;,cursor: default;,},.blocklyFlyoutButtonShadow {,fill: #666;,},.blocklyFlyoutButton:hover {,fill: #aaa;,},.blocklyFlyoutLabel {,cursor: default;,},.blocklyFlyoutLabelBackground {,opacity: 0;,}".split(","));Blockly.Generator=function(a){this.name_=a;this.FUNCTION_NAME_PLACEHOLDER_REGEXP_=new RegExp(this.FUNCTION_NAME_PLACEHOLDER_,"g")};Blockly.Generator.NAME_TYPE="generated_function";Blockly.Generator.prototype.INFINITE_LOOP_TRAP=null;Blockly.Generator.prototype.STATEMENT_PREFIX=null;Blockly.Generator.prototype.STATEMENT_SUFFIX=null;Blockly.Generator.prototype.INDENT=" ";Blockly.Generator.prototype.COMMENT_WRAP=60;Blockly.Generator.prototype.ORDER_OVERRIDES=[]; Blockly.Generator.prototype.workspaceToCode=function(a){a||(console.warn("No workspace specified in workspaceToCode call. Guessing."),a=Blockly.getMainWorkspace());var b=[];this.init(a);a=a.getTopBlocks(!0);for(var c=0,d;d=a[c];c++){var e=this.blockToCode(d);Array.isArray(e)&&(e=e[0]);e&&(d.outputConnection&&(e=this.scrubNakedValue(e),this.STATEMENT_PREFIX&&!d.suppressPrefixSuffix&&(e=this.injectId(this.STATEMENT_PREFIX,d)+e),this.STATEMENT_SUFFIX&&!d.suppressPrefixSuffix&&(e+=this.injectId(this.STATEMENT_SUFFIX, d))),b.push(e))}b=b.join("\n");b=this.finish(b);b=b.replace(/^\s+\n/,"");b=b.replace(/\n\s+$/,"\n");return b=b.replace(/[ \t]+\n/g,"\n")};Blockly.Generator.prototype.prefixLines=function(a,b){return b+a.replace(/(?!\n$)\n/g,"\n"+b)};Blockly.Generator.prototype.allNestedComments=function(a){var b=[];a=a.getDescendants(!0);for(var c=0;ca&&(a=(a=this.getParent())?a.getDepth()+1:0,this.setDepth_(a));return a};Blockly.tree.BaseNode.prototype.setDepth_=function(a){if(a!=this.depth_){this.depth_=a;var b=this.getRowElement();if(b){var c=this.getPixelIndent_()+"px";this.rightToLeft_?b.style.paddingRight=c:b.style.paddingLeft=c}this.forEachChild(function(b){b.setDepth_(a+1)})}};Blockly.tree.BaseNode.prototype.contains=function(a){for(;a;){if(a==this)return!0;a=a.getParent()}return!1}; -Blockly.tree.BaseNode.prototype.getChildren=function(){var a=[];this.forEachChild(function(b){a.push(b)});return a};Blockly.tree.BaseNode.prototype.getPreviousSibling=function(){return this.previousSibling_};Blockly.tree.BaseNode.prototype.getNextSibling=function(){return this.nextSibling_};Blockly.tree.BaseNode.prototype.isLastSibling=function(){return!this.nextSibling_};Blockly.tree.BaseNode.prototype.isSelected=function(){return this.selected_}; -Blockly.tree.BaseNode.prototype.select=function(){var a=this.getTree();a&&a.setSelectedItem(this)};Blockly.tree.BaseNode.prototype.setSelected=function(a){if(this.selected_!=a){this.selected_=a;this.updateRow();var b=this.getElement();b&&(Blockly.utils.aria.setState(b,Blockly.utils.aria.State.SELECTED,a),a&&(a=this.getTree().getElement(),Blockly.utils.aria.setState(a,Blockly.utils.aria.State.ACTIVEDESCENDANT,this.getId())))}}; +Blockly.tree.BaseNode.prototype.getChildren=function(){var a=[];this.forEachChild(function(b){a.push(b)});return a};Blockly.tree.BaseNode.prototype.getParent=function(){return Blockly.tree.BaseNode.superClass_.getParent.call(this)};Blockly.tree.BaseNode.prototype.getPreviousSibling=function(){return this.previousSibling_};Blockly.tree.BaseNode.prototype.getNextSibling=function(){return this.nextSibling_};Blockly.tree.BaseNode.prototype.isLastSibling=function(){return!this.nextSibling_}; +Blockly.tree.BaseNode.prototype.isSelected=function(){return this.selected_};Blockly.tree.BaseNode.prototype.select=function(){var a=this.getTree();a&&a.setSelectedItem(this)};Blockly.tree.BaseNode.prototype.setSelected=function(a){if(this.selected_!=a){this.selected_=a;this.updateRow();var b=this.getElement();b&&(Blockly.utils.aria.setState(b,Blockly.utils.aria.State.SELECTED,a),a&&(a=this.getTree().getElement(),Blockly.utils.aria.setState(a,Blockly.utils.aria.State.ACTIVEDESCENDANT,this.getId())))}}; Blockly.tree.BaseNode.prototype.setExpanded=function(a){var b=a!=this.expanded_,c;this.expanded_=a;var d=this.getTree(),e=this.getElement();if(this.hasChildren()){if(!a&&d&&this.contains(d.getSelectedItem())&&this.select(),e){if(c=this.getChildrenElement())Blockly.utils.style.setElementShown(c,a),Blockly.utils.aria.setState(e,Blockly.utils.aria.State.EXPANDED,a),a&&this.isInDocument()&&!c.hasChildNodes()&&(this.forEachChild(function(a){c.appendChild(a.toDom())}),this.forEachChild(function(a){a.enterDocument()})); this.updateExpandIcon()}}else(c=this.getChildrenElement())&&Blockly.utils.style.setElementShown(c,!1);e&&this.updateIcon_();b&&(a?this.doNodeExpanded():this.doNodeCollapsed())};Blockly.tree.BaseNode.prototype.doNodeExpanded=function(){};Blockly.tree.BaseNode.prototype.doNodeCollapsed=function(){};Blockly.tree.BaseNode.prototype.toggle=function(){this.setExpanded(!this.expanded_)}; Blockly.tree.BaseNode.prototype.toDom=function(){var a=this.expanded_&&this.hasChildren(),b=document.createElement("div");b.style.backgroundPosition=this.getBackgroundPosition();a||(b.style.display="none");a&&this.forEachChild(function(a){b.appendChild(a.toDom())});a=document.createElement("div");a.id=this.getId();a.appendChild(this.getRowDom());a.appendChild(b);return a};Blockly.tree.BaseNode.prototype.getPixelIndent_=function(){return Math.max(0,(this.getDepth()-1)*this.config_.indentWidth)}; Blockly.tree.BaseNode.prototype.getRowDom=function(){var a=document.createElement("div");a.className=this.getRowClassName();a.style["padding-"+(this.rightToLeft_?"right":"left")]=this.getPixelIndent_()+"px";a.appendChild(this.getIconDom());a.appendChild(this.getLabelDom());return a};Blockly.tree.BaseNode.prototype.getRowClassName=function(){var a="";this.isSelected()&&(a=" "+(this.config_.cssSelectedRow||""));return this.config_.cssTreeRow+a}; -Blockly.tree.BaseNode.prototype.getLabelDom=function(){var a=document.createElement("span");a.className=this.config_.cssItemLabel||"";a.textContent=this.content;return a};Blockly.tree.BaseNode.prototype.getIconDom=function(){var a=document.createElement("span");a.style.display="inline-block";a.className=this.getCalculatedIconClass();return a};Blockly.tree.BaseNode.prototype.getCalculatedIconClass=function(){throw Error("unimplemented abstract method");}; +Blockly.tree.BaseNode.prototype.getLabelDom=function(){var a=document.createElement("span");a.className=this.config_.cssItemLabel||"";a.textContent=this.content;return a};Blockly.tree.BaseNode.prototype.getIconDom=function(){var a=document.createElement("span");a.style.display="inline-block";a.className=this.getCalculatedIconClass();return a};Blockly.tree.BaseNode.prototype.getCalculatedIconClass=function(){throw Error(Blockly.Component.Error.ABSTRACT_METHOD);}; Blockly.tree.BaseNode.prototype.getBackgroundPosition=function(){return(this.isLastSibling()?"-100":(this.getDepth()-1)*this.config_.indentWidth)+"px 0"};Blockly.tree.BaseNode.prototype.getElement=function(){var a=Blockly.tree.BaseNode.superClass_.getElement.call(this);a||(a=document.getElementById(this.getId()),this.setElementInternal(a));return a};Blockly.tree.BaseNode.prototype.getRowElement=function(){var a=this.getElement();return a?a.firstChild:null}; Blockly.tree.BaseNode.prototype.getIconElement=function(){var a=this.getRowElement();return a?a.firstChild:null};Blockly.tree.BaseNode.prototype.getLabelElement=function(){var a=this.getRowElement();return a&&a.lastChild?a.lastChild.previousSibling:null};Blockly.tree.BaseNode.prototype.getChildrenElement=function(){var a=this.getElement();return a?a.lastChild:null};Blockly.tree.BaseNode.prototype.updateRow=function(){var a=this.getRowElement();a&&(a.className=this.getRowClassName())}; -Blockly.tree.BaseNode.prototype.updateExpandIcon=function(){var a=this.getChildrenElement();a&&(a.style.backgroundPosition=this.getBackgroundPosition())};Blockly.tree.BaseNode.prototype.updateIcon_=function(){this.getIconElement().className=this.getCalculatedIconClass()};Blockly.tree.BaseNode.prototype.onMouseDown=function(a){"expand"==a.target.getAttribute("type")&&this.hasChildren()?this.isUserCollapsible_&&this.toggle():(this.select(),this.updateRow())}; -Blockly.tree.BaseNode.prototype.onClick_=function(a){a.preventDefault()};Blockly.tree.BaseNode.prototype.onKeyDown=function(a){var b=!0;switch(a.keyCode){case Blockly.utils.KeyCodes.RIGHT:if(a.altKey)break;b=this.selectChild();break;case Blockly.utils.KeyCodes.LEFT:if(a.altKey)break;b=this.selectParent();break;case Blockly.utils.KeyCodes.DOWN:b=this.selectNext();break;case Blockly.utils.KeyCodes.UP:b=this.selectPrevious();break;default:b=!1}b&&a.preventDefault();return b}; -Blockly.tree.BaseNode.prototype.selectNext=function(){var a=this.getNextShownNode();a&&a.select();return!0};Blockly.tree.BaseNode.prototype.selectPrevious=function(){var a=this.getPreviousShownNode();a&&a.select();return!0};Blockly.tree.BaseNode.prototype.selectParent=function(){if(this.hasChildren()&&this.expanded_&&this.isUserCollapsible_)this.setExpanded(!1);else{var a=this.getParent(),b=this.getTree();a&&a!=b&&a.select()}return!0}; +Blockly.tree.BaseNode.prototype.updateExpandIcon=function(){var a=this.getChildrenElement();a&&(a.style.backgroundPosition=this.getBackgroundPosition())};Blockly.tree.BaseNode.prototype.updateIcon_=function(){this.getIconElement().className=this.getCalculatedIconClass()};Blockly.tree.BaseNode.prototype.onClick_=function(a){a.preventDefault()}; +Blockly.tree.BaseNode.prototype.onKeyDown=function(a){switch(a.keyCode){case Blockly.utils.KeyCodes.RIGHT:var b=this.selectChild();break;case Blockly.utils.KeyCodes.LEFT:b=this.selectParent();break;case Blockly.utils.KeyCodes.DOWN:b=this.selectNext();break;case Blockly.utils.KeyCodes.UP:b=this.selectPrevious();break;case Blockly.utils.KeyCodes.ENTER:case Blockly.utils.KeyCodes.SPACE:this.toggle();b=!0;break;default:b=!1}b&&a.preventDefault();return b}; +Blockly.tree.BaseNode.prototype.selectNext=function(){var a=this.getNextShownNode();a&&a.select();return!0};Blockly.tree.BaseNode.prototype.selectPrevious=function(){var a=this.getPreviousShownNode();a&&a.select();return!0};Blockly.tree.BaseNode.prototype.selectParent=function(){if(this.hasChildren()&&this.expanded_)this.setExpanded(!1);else{var a=this.getParent(),b=this.getTree();a&&a!=b&&a.select()}return!0}; Blockly.tree.BaseNode.prototype.selectChild=function(){return this.hasChildren()?(this.expanded_?this.getChildAt(0).select():this.setExpanded(!0),!0):!1};Blockly.tree.BaseNode.prototype.getLastShownDescendant=function(){return this.expanded_&&this.hasChildren()?this.getChildAt(this.getChildCount()-1).getLastShownDescendant():this}; Blockly.tree.BaseNode.prototype.getNextShownNode=function(){if(this.hasChildren()&&this.expanded_)return this.getChildAt(0);for(var a=this,b;a!=this.getTree();){b=a.getNextSibling();if(null!=b)return b;a=a.getParent()}return null};Blockly.tree.BaseNode.prototype.getPreviousShownNode=function(){var a=this.getPreviousSibling();if(null!=a)return a.getLastShownDescendant();a=this.getParent();var b=this.getTree();return a==b||this==b?null:a}; -Blockly.tree.BaseNode.prototype.setTreeInternal=function(a){this.tree!=a&&(this.tree=a,this.forEachChild(function(b){b.setTreeInternal(a)}))};Blockly.tree.TreeNode=function(a,b,c){this.toolbox_=a;Blockly.tree.BaseNode.call(this,b,c)};Blockly.utils.object.inherits(Blockly.tree.TreeNode,Blockly.tree.BaseNode);Blockly.tree.TreeNode.prototype.getTree=function(){if(this.tree)return this.tree;var a=this.getParent();return a&&(a=a.getTree())?(this.setTreeInternal(a),a):null}; +Blockly.tree.BaseNode.prototype.setTreeInternal=function(a){this.tree!=a&&(this.tree=a,this.forEachChild(function(b){b.setTreeInternal(a)}))};Blockly.tree.TreeNode=function(a,b,c){this.toolbox_=a;Blockly.tree.BaseNode.call(this,b,c);this.onSizeChanged_=null};Blockly.utils.object.inherits(Blockly.tree.TreeNode,Blockly.tree.BaseNode);Blockly.tree.TreeNode.prototype.getTree=function(){if(this.tree)return this.tree;var a=this.getParent();return a&&(a=a.getTree())?(this.setTreeInternal(a),a):null}; Blockly.tree.TreeNode.prototype.getCalculatedIconClass=function(){var a=this.expanded_;if(a&&this.expandedIconClass)return this.expandedIconClass;var b=this.iconClass;if(!a&&b)return b;b=this.config_;if(this.hasChildren()){if(a&&b.cssExpandedFolderIcon)return b.cssTreeIcon+" "+b.cssExpandedFolderIcon;if(!a&&b.cssCollapsedFolderIcon)return b.cssTreeIcon+" "+b.cssCollapsedFolderIcon}else if(b.cssFileIcon)return b.cssTreeIcon+" "+b.cssFileIcon;return""}; -Blockly.tree.TreeNode.prototype.onClick_=function(a){this.hasChildren()&&this.isUserCollapsible_?(this.toggle(),this.select()):this.isSelected()?this.getTree().setSelectedItem(null):this.select();this.updateRow()};Blockly.tree.TreeNode.prototype.onMouseDown=function(a){}; +Blockly.tree.TreeNode.prototype.onClick_=function(a){this.hasChildren()?(this.toggle(),this.select()):this.isSelected()?this.getTree().setSelectedItem(null):this.select();this.updateRow()}; Blockly.tree.TreeNode.prototype.onKeyDown=function(a){if(this.tree.toolbox_.horizontalLayout_){var b={},c=Blockly.utils.KeyCodes.DOWN,d=Blockly.utils.KeyCodes.UP;b[Blockly.utils.KeyCodes.RIGHT]=this.rightToLeft_?d:c;b[Blockly.utils.KeyCodes.LEFT]=this.rightToLeft_?c:d;b[Blockly.utils.KeyCodes.UP]=Blockly.utils.KeyCodes.LEFT;b[Blockly.utils.KeyCodes.DOWN]=Blockly.utils.KeyCodes.RIGHT;Object.defineProperties(a,{keyCode:{value:b[a.keyCode]||a.keyCode}})}return Blockly.tree.TreeNode.superClass_.onKeyDown.call(this, -a)};Blockly.tree.TreeNode.prototype.onSizeChanged=function(a){this.onSizeChanged_=a};Blockly.tree.TreeNode.prototype.resizeToolbox_=function(){this.onSizeChanged_&&this.onSizeChanged_.call(this.toolbox_)};Blockly.tree.TreeNode.prototype.doNodeExpanded=Blockly.tree.TreeNode.prototype.resizeToolbox_;Blockly.tree.TreeNode.prototype.doNodeCollapsed=Blockly.tree.TreeNode.prototype.resizeToolbox_;Blockly.tree.TreeControl=function(a,b){this.toolbox_=a;this.onKeydownWrapper_=this.onClickWrapper_=this.onBlurWrapper_=this.onFocusWrapper_=null;Blockly.tree.BaseNode.call(this,"",b);this.selected_=this.expanded_=!0;this.selectedItem_=this};Blockly.utils.object.inherits(Blockly.tree.TreeControl,Blockly.tree.BaseNode);Blockly.tree.TreeControl.prototype.getTree=function(){return this};Blockly.tree.TreeControl.prototype.getToolbox=function(){return this.toolbox_}; -Blockly.tree.TreeControl.prototype.getDepth=function(){return 0};Blockly.tree.TreeControl.prototype.handleFocus_=function(a){this.focused_=!0;a=this.getElement();Blockly.utils.dom.addClass(a,"focused");this.selectedItem_&&this.selectedItem_.select()};Blockly.tree.TreeControl.prototype.handleBlur_=function(a){this.focused_=!1;a=this.getElement();Blockly.utils.dom.removeClass(a,"focused")};Blockly.tree.TreeControl.prototype.hasFocus=function(){return this.focused_}; -Blockly.tree.TreeControl.prototype.setExpanded=function(a){this.expanded_=a};Blockly.tree.TreeControl.prototype.getIconElement=function(){var a=this.getRowElement();return a?a.firstChild:null};Blockly.tree.TreeControl.prototype.updateExpandIcon=function(){};Blockly.tree.TreeControl.prototype.getRowClassName=function(){return Blockly.tree.TreeControl.superClass_.getRowClassName.call(this)+" "+this.config_.cssHideRoot}; -Blockly.tree.TreeControl.prototype.getCalculatedIconClass=function(){var a=this.expanded_;if(a&&this.expandedIconClass)return this.expandedIconClass;var b=this.iconClass;return!a&&b?b:a&&this.config_.cssExpandedRootIcon?this.config_.cssTreeIcon+" "+this.config_.cssExpandedRootIcon:""}; +a)};Blockly.tree.TreeNode.prototype.onSizeChanged=function(a){this.onSizeChanged_=a};Blockly.tree.TreeNode.prototype.resizeToolbox_=function(){this.onSizeChanged_&&this.onSizeChanged_.call(this.toolbox_)};Blockly.tree.TreeNode.prototype.doNodeExpanded=Blockly.tree.TreeNode.prototype.resizeToolbox_;Blockly.tree.TreeNode.prototype.doNodeCollapsed=Blockly.tree.TreeNode.prototype.resizeToolbox_;Blockly.tree.TreeControl=function(a,b){this.toolbox_=a;this.onKeydownWrapper_=this.onClickWrapper_=null;Blockly.tree.BaseNode.call(this,"",b);this.selected_=this.expanded_=!0;this.selectedItem_=this;this.onAfterSelected_=this.onBeforeSelected_=null};Blockly.utils.object.inherits(Blockly.tree.TreeControl,Blockly.tree.BaseNode);Blockly.tree.TreeControl.prototype.getTree=function(){return this};Blockly.tree.TreeControl.prototype.getToolbox=function(){return this.toolbox_}; +Blockly.tree.TreeControl.prototype.getDepth=function(){return 0};Blockly.tree.TreeControl.prototype.setExpanded=function(a){this.expanded_=a};Blockly.tree.TreeControl.prototype.getIconElement=function(){var a=this.getRowElement();return a?a.firstChild:null};Blockly.tree.TreeControl.prototype.updateExpandIcon=function(){};Blockly.tree.TreeControl.prototype.getRowClassName=function(){return Blockly.tree.TreeControl.superClass_.getRowClassName.call(this)+" "+this.config_.cssHideRoot}; +Blockly.tree.TreeControl.prototype.getCalculatedIconClass=function(){var a=this.expanded_;if(a&&this.expandedIconClass)return this.expandedIconClass;var b=this.iconClass;return!a&&b?b:""}; Blockly.tree.TreeControl.prototype.setSelectedItem=function(a){if(a!=this.selectedItem_&&(!this.onBeforeSelected_||this.onBeforeSelected_.call(this.toolbox_,a))){var b=this.getSelectedItem();this.selectedItem_&&this.selectedItem_.setSelected(!1);(this.selectedItem_=a)&&a.setSelected(!0);this.onAfterSelected_&&this.onAfterSelected_.call(this.toolbox_,b,a)}};Blockly.tree.TreeControl.prototype.onBeforeSelected=function(a){this.onBeforeSelected_=a}; Blockly.tree.TreeControl.prototype.onAfterSelected=function(a){this.onAfterSelected_=a};Blockly.tree.TreeControl.prototype.getSelectedItem=function(){return this.selectedItem_};Blockly.tree.TreeControl.prototype.initAccessibility=function(){Blockly.tree.TreeControl.superClass_.initAccessibility.call(this);var a=this.getElement();Blockly.utils.aria.setRole(a,Blockly.utils.aria.Role.TREE);Blockly.utils.aria.setState(a,Blockly.utils.aria.State.LABELLEDBY,this.getLabelElement().id)}; Blockly.tree.TreeControl.prototype.enterDocument=function(){Blockly.tree.TreeControl.superClass_.enterDocument.call(this);var a=this.getElement();a.className=this.config_.cssRoot;a.setAttribute("hideFocus","true");this.attachEvents_();this.initAccessibility()};Blockly.tree.TreeControl.prototype.exitDocument=function(){Blockly.tree.TreeControl.superClass_.exitDocument.call(this);this.detachEvents_()}; -Blockly.tree.TreeControl.prototype.attachEvents_=function(){var a=this.getElement();a.tabIndex=0;this.onFocusWrapper_=Blockly.bindEvent_(a,"focus",this,this.handleFocus_);this.onBlurWrapper_=Blockly.bindEvent_(a,"blur",this,this.handleBlur_);this.onClickWrapper_=Blockly.bindEventWithChecks_(a,"click",this,this.handleMouseEvent_);this.onKeydownWrapper_=Blockly.bindEvent_(a,"keydown",this,this.handleKeyEvent_)}; -Blockly.tree.TreeControl.prototype.detachEvents_=function(){this.onFocusWrapper_&&(Blockly.unbindEvent_(this.onFocusWrapper_),this.onFocusWrapper_=null);this.onBlurWrapper_&&(Blockly.unbindEvent_(this.onBlurWrapper_),this.onBlurWrapper_=null);this.onClickWrapper_&&(Blockly.unbindEvent_(this.onClickWrapper_),this.onClickWrapper_=null);this.onKeydownWrapper_&&(Blockly.unbindEvent_(this.onKeydownWrapper_),this.onKeydownWrapper_=null)}; -Blockly.tree.TreeControl.prototype.handleMouseEvent_=function(a){var b=this.getNodeFromEvent_(a);if(b)switch(a.type){case "mousedown":b.onMouseDown(a);break;case "click":b.onClick_(a)}};Blockly.tree.TreeControl.prototype.handleKeyEvent_=function(a){var b=!1;if(b=this.selectedItem_&&this.selectedItem_.onKeyDown(a)||b)Blockly.utils.style.scrollIntoContainerView(this.selectedItem_.getElement(),this.getElement().parentNode),a.preventDefault();return b}; -Blockly.tree.TreeControl.prototype.getNodeFromEvent_=function(a){for(var b=a.target;null!=b;){if(a=Blockly.tree.BaseNode.allNodes[b.id])return a;if(b==this.getElement())break;if(b.getAttribute("role")==Blockly.utils.aria.Role.GROUP)break;b=b.parentNode}return null};Blockly.tree.TreeControl.prototype.createNode=function(a){return new Blockly.tree.TreeNode(this.toolbox_,a||"",this.config_)};Blockly.Toolbox=function(a){this.workspace_=a;this.RTL=a.options.RTL;this.horizontalLayout_=a.options.horizontalLayout;this.toolboxPosition=a.options.toolboxPosition;this.config_={indentWidth:19,cssRoot:"blocklyTreeRoot",cssHideRoot:"blocklyHidden",cssTreeRow:"blocklyTreeRow",cssItemLabel:"blocklyTreeLabel",cssTreeIcon:"blocklyTreeIcon",cssExpandedFolderIcon:"blocklyTreeIconOpen",cssFileIcon:"blocklyTreeIconNone",cssSelectedRow:"blocklyTreeSelected"};this.treeSeparatorConfig_={cssTreeRow:"blocklyTreeSeparator"}; -this.horizontalLayout_&&(this.config_.cssTreeRow+=a.RTL?" blocklyHorizontalTreeRtl":" blocklyHorizontalTree",this.treeSeparatorConfig_.cssTreeRow="blocklyTreeSeparatorHorizontal "+(a.RTL?"blocklyHorizontalTreeRtl":"blocklyHorizontalTree"),this.config_.cssTreeIcon="");this.flyout_=null};Blockly.Toolbox.prototype.width=0;Blockly.Toolbox.prototype.height=0;Blockly.Toolbox.prototype.selectedOption_=null;Blockly.Toolbox.prototype.lastCategory_=null; +Blockly.tree.TreeControl.prototype.attachEvents_=function(){var a=this.getElement();a.tabIndex=0;this.onClickWrapper_=Blockly.bindEventWithChecks_(a,"click",this,this.handleMouseEvent_);this.onKeydownWrapper_=Blockly.bindEvent_(a,"keydown",this,this.handleKeyEvent_)}; +Blockly.tree.TreeControl.prototype.detachEvents_=function(){this.onClickWrapper_&&(Blockly.unbindEvent_(this.onClickWrapper_),this.onClickWrapper_=null);this.onKeydownWrapper_&&(Blockly.unbindEvent_(this.onKeydownWrapper_),this.onKeydownWrapper_=null)};Blockly.tree.TreeControl.prototype.handleMouseEvent_=function(a){var b=this.getNodeFromEvent_(a);if(b&&"click"==a.type)b.onClick_(a)}; +Blockly.tree.TreeControl.prototype.handleKeyEvent_=function(a){var b=!(!this.selectedItem_||!this.selectedItem_.onKeyDown(a));b&&(Blockly.utils.style.scrollIntoContainerView(this.selectedItem_.getElement(),this.getElement().parentNode),a.preventDefault());return b};Blockly.tree.TreeControl.prototype.getNodeFromEvent_=function(a){for(var b=a.target;b;){if(a=Blockly.tree.BaseNode.allNodes[b.id])return a;if(b==this.getElement())break;if(b.getAttribute("role")==Blockly.utils.aria.Role.GROUP)break;b=b.parentNode}return null}; +Blockly.tree.TreeControl.prototype.createNode=function(a){return new Blockly.tree.TreeNode(this.toolbox_,a||"",this.config_)};Blockly.Toolbox=function(a){this.workspace_=a;this.RTL=a.options.RTL;this.horizontalLayout_=a.options.horizontalLayout;this.toolboxPosition=a.options.toolboxPosition;this.config_={indentWidth:19,cssRoot:"blocklyTreeRoot",cssHideRoot:"blocklyHidden",cssTreeRow:"blocklyTreeRow",cssItemLabel:"blocklyTreeLabel",cssTreeIcon:"blocklyTreeIcon",cssExpandedFolderIcon:"blocklyTreeIconOpen",cssFileIcon:"blocklyTreeIconNone",cssSelectedRow:"blocklyTreeSelected"};this.treeSeparatorConfig_={cssTreeRow:"blocklyTreeSeparator"}; +this.horizontalLayout_&&(this.config_.cssTreeRow+=a.RTL?" blocklyHorizontalTreeRtl":" blocklyHorizontalTree",this.treeSeparatorConfig_.cssTreeRow="blocklyTreeSeparatorHorizontal "+(a.RTL?"blocklyHorizontalTreeRtl":"blocklyHorizontalTree"),this.config_.cssTreeIcon="");this.flyout_=null;this.height=this.width=0;this.lastCategory_=null}; Blockly.Toolbox.prototype.init=function(){var a=this.workspace_,b=this.workspace_.getParentSvg();this.HtmlDiv=document.createElement("div");this.HtmlDiv.className="blocklyToolboxDiv blocklyNonSelectable";this.HtmlDiv.setAttribute("dir",a.RTL?"RTL":"LTR");b.parentNode.insertBefore(this.HtmlDiv,b);var c=a.getThemeManager();c.subscribe(this.HtmlDiv,"toolboxBackgroundColour","background-color");c.subscribe(this.HtmlDiv,"toolboxForegroundColour","color");Blockly.bindEventWithChecks_(this.HtmlDiv,"mousedown", this,function(a){Blockly.utils.isRightButton(a)||a.target==this.HtmlDiv?Blockly.hideChaff(!1):Blockly.hideChaff(!0);Blockly.Touch.clearTouchIdentifier()},!1,!0);c=new Blockly.Options({parentWorkspace:a,rtl:a.RTL,oneBasedIndex:a.options.oneBasedIndex,horizontalLayout:a.horizontalLayout,renderer:a.options.renderer,rendererOverrides:a.options.rendererOverrides});c.toolboxPosition=a.options.toolboxPosition;if(a.horizontalLayout){if(!Blockly.HorizontalFlyout)throw Error("Missing require for Blockly.HorizontalFlyout"); -this.flyout_=new Blockly.HorizontalFlyout(c)}else{if(!Blockly.VerticalFlyout)throw Error("Missing require for Blockly.VerticalFlyout");this.flyout_=new Blockly.VerticalFlyout(c)}if(!this.flyout_)throw Error("One of Blockly.VerticalFlyout or Blockly.Horizontal must berequired.");Blockly.utils.dom.insertAfter(this.flyout_.createDom("svg"),b);this.flyout_.init(a);this.config_.cleardotPath=a.options.pathToMedia+"1x1.gif";this.config_.cssCollapsedFolderIcon="blocklyTreeIconClosed"+(a.RTL?"Rtl":"Ltr"); -this.renderTree(a.options.languageTree)}; -Blockly.Toolbox.prototype.renderTree=function(a){this.tree_&&(this.tree_.dispose(),this.lastCategory_=null);var b=new Blockly.tree.TreeControl(this,this.config_);this.tree_=b;b.setSelectedItem(null);b.onBeforeSelected(this.handleBeforeTreeSelected_);b.onAfterSelected(this.handleAfterTreeSelected_);var c=null;if(a){this.tree_.blocks=[];this.hasColours_=!1;c=this.syncTrees_(a,this.tree_,this.workspace_.options.pathToMedia);if(this.tree_.blocks.length)throw Error("Toolbox cannot have both blocks and categories in the root level.");this.workspace_.resizeContents()}b.render(this.HtmlDiv); -c&&b.setSelectedItem(c);this.addColour_();this.position();this.horizontalLayout_&&Blockly.utils.aria.setState(this.tree_.getElement(),Blockly.utils.aria.State.ORIENTATION,"horizontal")};Blockly.Toolbox.prototype.handleBeforeTreeSelected_=function(a){if(a==this.tree_)return!1;this.lastCategory_&&(this.lastCategory_.getRowElement().style.backgroundColor="");if(a){var b=a.hexColour||"#57e";a.getRowElement().style.backgroundColor=b;this.addColour_(a)}return!0}; -Blockly.Toolbox.prototype.handleAfterTreeSelected_=function(a,b){b&&b.blocks&&b.blocks.length?(this.flyout_.show(b.blocks),this.lastCategory_!=b&&this.flyout_.scrollToStart(),this.workspace_.keyboardAccessibilityMode&&Blockly.navigation.setState(Blockly.navigation.STATE_TOOLBOX)):(this.flyout_.hide(),!this.workspace_.keyboardAccessibilityMode||b instanceof Blockly.Toolbox.TreeSeparator||Blockly.navigation.setState(Blockly.navigation.STATE_WS));a!=b&&a!=this&&(a=new Blockly.Events.Ui(null,"category", +this.flyout_=new Blockly.HorizontalFlyout(c)}else{if(!Blockly.VerticalFlyout)throw Error("Missing require for Blockly.VerticalFlyout");this.flyout_=new Blockly.VerticalFlyout(c)}if(!this.flyout_)throw Error("One of Blockly.VerticalFlyout or Blockly.Horizontal must berequired.");Blockly.utils.dom.insertAfter(this.flyout_.createDom("svg"),b);this.flyout_.init(a);this.config_.cssCollapsedFolderIcon="blocklyTreeIconClosed"+(a.RTL?"Rtl":"Ltr");this.render(a.options.languageTree)}; +Blockly.Toolbox.prototype.render=function(a){this.tree_&&(this.tree_.dispose(),this.lastCategory_=null);var b=new Blockly.tree.TreeControl(this,this.config_);this.tree_=b;b.setSelectedItem(null);b.onBeforeSelected(this.handleBeforeTreeSelected_);b.onAfterSelected(this.handleAfterTreeSelected_);var c=null;if(a){this.tree_.contents=[];this.hasColours_=!1;c=this.createTree_(a,this.tree_);if(this.tree_.contents.length)throw Error("Toolbox cannot have both blocks and categories in the root level.");this.workspace_.resizeContents()}b.render(this.HtmlDiv); +c&&b.setSelectedItem(c);this.addColour_();this.position();this.horizontalLayout_&&Blockly.utils.aria.setState(this.tree_.getElement(),Blockly.utils.aria.State.ORIENTATION,"horizontal")}; +Blockly.Toolbox.prototype.createTree_=function(a,b){var c=null,d=null;if(!a)return null;for(var e=0,f;f=a[e];e++)switch(f.kind.toUpperCase()){case "CATEGORY":c=this.addCategory_(f,b)||c;d=f;break;case "SEP":d=this.addSeparator_(f,b,d)||d;break;case "BLOCK":case "SHADOW":case "LABEL":case "BUTTON":b.contents.push(f),d=f}return c}; +Blockly.Toolbox.prototype.addCategory_=function(a,b){var c=null,d=Blockly.utils.replaceMessageReferences(a.name),e=this.tree_.createNode(d);e.onSizeChanged(this.handleNodeSizeChanged_);e.contents=[];b.add(e);(b=a.custom)?e.contents=b:c=this.createTree_(a.contents,e)||c;this.setColourOrStyle_(a,e,d);return c=this.setExpanded_(a,e)||c}; +Blockly.Toolbox.prototype.setColourOrStyle_=function(a,b,c){var d=a.categorystyle;(a=a.colour)&&d?(b.hexColour="",console.warn('Toolbox category "'+c+'" must not have both a style and a colour')):d?this.setColourFromStyle_(d,b,c):this.setColour_(a,b,c)};Blockly.Toolbox.prototype.addSeparator_=function(a,b,c){if(c&&"CATEGORY"==c.kind.toUpperCase())b.add(new Blockly.Toolbox.TreeSeparator(this.treeSeparatorConfig_));else return b.contents.push(a),a;return null}; +Blockly.Toolbox.prototype.setExpanded_=function(a,b){var c=null;"true"==a.expanded?(b.contents.length&&(c=b),b.setExpanded(!0)):b.setExpanded(!1);return c};Blockly.Toolbox.prototype.handleBeforeTreeSelected_=function(a){if(a==this.tree_)return!1;this.lastCategory_&&(this.lastCategory_.getRowElement().style.backgroundColor="");if(a){var b=a.hexColour||"#57e";a.getRowElement().style.backgroundColor=b;this.addColour_(a)}return!0}; +Blockly.Toolbox.prototype.handleAfterTreeSelected_=function(a,b){b&&b.contents&&b.contents.length?(this.flyout_.show(b.contents),this.lastCategory_!=b&&this.flyout_.scrollToStart(),this.workspace_.keyboardAccessibilityMode&&Blockly.navigation.setState(Blockly.navigation.STATE_TOOLBOX)):(this.flyout_.hide(),!this.workspace_.keyboardAccessibilityMode||b instanceof Blockly.Toolbox.TreeSeparator||Blockly.navigation.setState(Blockly.navigation.STATE_WS));a!=b&&a!=this&&(a=new Blockly.Events.Ui(null,"category", a&&a.content,b&&b.content),a.workspaceId=this.workspace_.id,Blockly.Events.fire(a));b&&(this.lastCategory_=b)};Blockly.Toolbox.prototype.handleNodeSizeChanged_=function(){Blockly.svgResize(this.workspace_)}; Blockly.Toolbox.prototype.onBlocklyAction=function(a){var b=this.tree_.getSelectedItem();if(!b)return!1;switch(a.name){case Blockly.navigation.actionNames.PREVIOUS:return b.selectPrevious();case Blockly.navigation.actionNames.OUT:return b.selectParent();case Blockly.navigation.actionNames.NEXT:return b.selectNext();case Blockly.navigation.actionNames.IN:return b.selectChild();default:return!1}}; -Blockly.Toolbox.prototype.dispose=function(){this.flyout_.dispose();this.tree_.dispose();this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv);Blockly.utils.dom.removeNode(this.HtmlDiv);this.lastCategory_=null};Blockly.Toolbox.prototype.getWidth=function(){return this.width};Blockly.Toolbox.prototype.getHeight=function(){return this.height};Blockly.Toolbox.prototype.getFlyout=function(){return this.flyout_}; +Blockly.Toolbox.prototype.dispose=function(){this.flyout_.dispose();this.tree_.dispose();this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv);Blockly.utils.dom.removeNode(this.HtmlDiv);this.lastCategory_=null};Blockly.Toolbox.prototype.setVisible=function(a){this.HtmlDiv.style.display=a?"block":"none"};Blockly.Toolbox.prototype.getWidth=function(){return this.width};Blockly.Toolbox.prototype.getHeight=function(){return this.height};Blockly.Toolbox.prototype.getFlyout=function(){return this.flyout_}; Blockly.Toolbox.prototype.position=function(){var a=this.HtmlDiv;if(a){var b=Blockly.svgSize(this.workspace_.getParentSvg());this.horizontalLayout_?(a.style.left="0",a.style.height="auto",a.style.width=b.width+"px",this.height=a.offsetHeight,this.toolboxPosition==Blockly.TOOLBOX_AT_TOP?a.style.top="0":a.style.bottom="0"):(this.toolboxPosition==Blockly.TOOLBOX_AT_RIGHT?a.style.right="0":a.style.left="0",a.style.height=b.height+"px",this.width=a.offsetWidth);this.flyout_.position()}}; -Blockly.Toolbox.prototype.syncTrees_=function(a,b,c){for(var d=null,e=null,f=0,g;g=a.childNodes[f];f++)if(g.tagName)switch(g.tagName.toUpperCase()){case "CATEGORY":e=Blockly.utils.replaceMessageReferences(g.getAttribute("name"));var h=this.tree_.createNode(e);h.onSizeChanged(this.handleNodeSizeChanged_);h.blocks=[];b.add(h);var k=g.getAttribute("custom");k?h.blocks=k:(k=this.syncTrees_(g,h,c))&&(d=k);k=g.getAttribute("categorystyle");var l=g.getAttribute("colour");l&&k?(h.hexColour="",console.warn('Toolbox category "'+ -e+'" can not have both a style and a colour')):k?this.setColourFromStyle_(k,h,e):this.setColour_(l,h,e);"true"==g.getAttribute("expanded")?(h.blocks.length&&(d=h),h.setExpanded(!0)):h.setExpanded(!1);e=g;break;case "SEP":if(e&&"CATEGORY"==e.tagName.toUpperCase()){b.add(new Blockly.Toolbox.TreeSeparator(this.treeSeparatorConfig_));break}case "BLOCK":case "SHADOW":case "LABEL":case "BUTTON":b.blocks.push(g),e=g}return d}; Blockly.Toolbox.prototype.setColour_=function(a,b,c){a=Blockly.utils.replaceMessageReferences(a);if(null===a||""===a)b.hexColour="";else{var d=Number(a);isNaN(d)?(d=Blockly.utils.colour.parse(a))?(b.hexColour=d,this.hasColours_=!0):(b.hexColour="",console.warn('Toolbox category "'+c+'" has unrecognized colour attribute: '+a)):(b.hexColour=Blockly.hueToHex(d),this.hasColours_=!0)}}; Blockly.Toolbox.prototype.setColourFromStyle_=function(a,b,c){b.styleName=a;var d=this.workspace_.getTheme();a&&d&&((d=d.categoryStyles[a])&&d.colour?this.setColour_(d.colour,b,c):console.warn('Style "'+a+'" must exist and contain a colour value'))};Blockly.Toolbox.prototype.updateColourFromTheme_=function(a){if(a=a||this.tree_){a=a.getChildren(!1);for(var b=0,c;c=a[b];b++)c.styleName&&(this.setColourFromStyle_(c.styleName,c,""),this.addColour_()),this.updateColourFromTheme_(c)}}; -Blockly.Toolbox.prototype.updateColourFromTheme=function(){var a=this.tree_;a&&(this.updateColourFromTheme_(a),this.updateSelectedItemColour_(a))};Blockly.Toolbox.prototype.updateSelectedItemColour_=function(a){if(a=a.getSelectedItem()){var b=a.hexColour||"#57e";a.getRowElement().style.backgroundColor=b;this.addColour_(a)}}; +Blockly.Toolbox.prototype.refreshTheme=function(){var a=this.tree_;a&&(this.updateColourFromTheme_(a),this.updateSelectedItemColour_(a))};Blockly.Toolbox.prototype.updateSelectedItemColour_=function(a){if(a=a.getSelectedItem()){var b=a.hexColour||"#57e";a.getRowElement().style.backgroundColor=b;this.addColour_(a)}}; Blockly.Toolbox.prototype.addColour_=function(a){a=(a||this.tree_).getChildren(!1);for(var b=0,c;c=a[b];b++){var d=c.getRowElement();if(d){var e=this.hasColours_?"8px solid "+(c.hexColour||"#ddd"):"none";this.workspace_.RTL?d.style.borderRight=e:d.style.borderLeft=e}this.addColour_(c)}};Blockly.Toolbox.prototype.clearSelection=function(){this.tree_.setSelectedItem(null)};Blockly.Toolbox.prototype.addStyle=function(a){Blockly.utils.dom.addClass(this.HtmlDiv,a)}; Blockly.Toolbox.prototype.removeStyle=function(a){Blockly.utils.dom.removeClass(this.HtmlDiv,a)}; Blockly.Toolbox.prototype.getClientRect=function(){if(!this.HtmlDiv)return null;var a=this.HtmlDiv.getBoundingClientRect(),b=a.top,c=b+a.height,d=a.left;a=d+a.width;return this.toolboxPosition==Blockly.TOOLBOX_AT_TOP?new Blockly.utils.Rect(-1E7,c,-1E7,1E7):this.toolboxPosition==Blockly.TOOLBOX_AT_BOTTOM?new Blockly.utils.Rect(b,1E7,-1E7,1E7):this.toolboxPosition==Blockly.TOOLBOX_AT_LEFT?new Blockly.utils.Rect(-1E7,1E7,-1E7,a):new Blockly.utils.Rect(-1E7,1E7,d,1E7)}; -Blockly.Toolbox.prototype.refreshSelection=function(){var a=this.tree_.getSelectedItem();a&&a.blocks&&this.flyout_.show(a.blocks)};Blockly.Toolbox.prototype.selectFirstCategory=function(){this.tree_.getSelectedItem()||this.tree_.selectChild()};Blockly.Toolbox.TreeSeparator=function(a){Blockly.tree.TreeNode.call(this,null,"",a)};Blockly.utils.object.inherits(Blockly.Toolbox.TreeSeparator,Blockly.tree.TreeNode); +Blockly.Toolbox.prototype.refreshSelection=function(){var a=this.tree_.getSelectedItem();a&&a.contents&&this.flyout_.show(a.contents)};Blockly.Toolbox.prototype.selectFirstCategory=function(){this.tree_.getSelectedItem()||this.tree_.selectChild()};Blockly.Toolbox.TreeSeparator=function(a){Blockly.tree.TreeNode.call(this,null,"",a)};Blockly.utils.object.inherits(Blockly.Toolbox.TreeSeparator,Blockly.tree.TreeNode); Blockly.Css.register([".blocklyToolboxDelete {",'cursor: url("<<>>/handdelete.cur"), auto;',"}",".blocklyToolboxGrab {",'cursor: url("<<>>/handclosed.cur"), auto;',"cursor: grabbing;","cursor: -webkit-grabbing;","}",".blocklyToolboxDiv {","background-color: #ddd;","overflow-x: visible;","overflow-y: auto;","position: absolute;","z-index: 70;","-webkit-tap-highlight-color: transparent;","}",".blocklyTreeRoot {","padding: 4px 0;","}",".blocklyTreeRoot:focus {","outline: none;","}",".blocklyTreeRow {", "height: 22px;","line-height: 22px;","margin-bottom: 3px;","padding-right: 8px;","white-space: nowrap;","}",".blocklyHorizontalTree {","float: left;","margin: 1px 5px 8px 0;","}",".blocklyHorizontalTreeRtl {","float: right;","margin: 1px 0 8px 5px;","}",'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {',"margin-left: 8px;","}",".blocklyTreeRow:not(.blocklyTreeSelected):hover {","background-color: rgba(255, 255, 255, 0.2);","}",".blocklyTreeSeparator {","border-bottom: solid #e5e5e5 1px;","height: 0;", "margin: 5px 0;","}",".blocklyTreeSeparatorHorizontal {","border-right: solid #e5e5e5 1px;","width: 0;","padding: 5px 0;","margin: 0 5px;","}",".blocklyTreeIcon {","background-image: url(<<>>/sprites.png);","height: 16px;","vertical-align: middle;","width: 16px;","}",".blocklyTreeIconClosedLtr {","background-position: -32px -1px;","}",".blocklyTreeIconClosedRtl {","background-position: 0 -1px;","}",".blocklyTreeIconOpen {","background-position: -16px -1px;","}",".blocklyTreeSelected>.blocklyTreeIconClosedLtr {", -"background-position: -32px -17px;","}",".blocklyTreeSelected>.blocklyTreeIconClosedRtl {","background-position: 0 -17px;","}",".blocklyTreeSelected>.blocklyTreeIconOpen {","background-position: -16px -17px;","}",".blocklyTreeIconNone,",".blocklyTreeSelected>.blocklyTreeIconNone {","background-position: -48px -1px;","}",".blocklyTreeLabel {","cursor: default;","font-family: sans-serif;","font-size: 16px;","padding: 0 3px;","vertical-align: middle;","}",".blocklyToolboxDelete .blocklyTreeLabel {", -'cursor: url("<<>>/handdelete.cur"), auto;',"}",".blocklyTreeSelected .blocklyTreeLabel {","color: #fff;","}"]);Blockly.Trashcan=function(a){this.workspace_=a;this.contents_=[];this.flyout=null;if(!(0>=this.workspace_.options.maxTrashcanContents)){a=new Blockly.Options({scrollbars:!0,parentWorkspace:this.workspace_,rtl:this.workspace_.RTL,oneBasedIndex:this.workspace_.options.oneBasedIndex,renderer:this.workspace_.options.renderer,rendererOverrides:this.workspace_.options.rendererOverrides});if(this.workspace_.horizontalLayout){a.toolboxPosition=this.workspace_.toolboxPosition==Blockly.TOOLBOX_AT_TOP?Blockly.TOOLBOX_AT_BOTTOM: +"background-position: -32px -17px;","}",".blocklyTreeSelected>.blocklyTreeIconClosedRtl {","background-position: 0 -17px;","}",".blocklyTreeSelected>.blocklyTreeIconOpen {","background-position: -16px -17px;","}",".blocklyTreeIconNone,",".blocklyTreeSelected>.blocklyTreeIconNone {","background-position: -48px -1px;","}",".blocklyTreeLabel {","cursor: default;","font: 16px sans-serif;","padding: 0 3px;","vertical-align: middle;","}",".blocklyToolboxDelete .blocklyTreeLabel {",'cursor: url("<<>>/handdelete.cur"), auto;', +"}",".blocklyTreeSelected .blocklyTreeLabel {","color: #fff;","}"]);Blockly.registry.register(Blockly.registry.Type.TOOLBOX,Blockly.registry.DEFAULT,Blockly.Toolbox);Blockly.Trashcan=function(a){this.workspace_=a;this.contents_=[];this.flyout=null;if(!(0>=this.workspace_.options.maxTrashcanContents)){a=new Blockly.Options({scrollbars:!0,parentWorkspace:this.workspace_,rtl:this.workspace_.RTL,oneBasedIndex:this.workspace_.options.oneBasedIndex,renderer:this.workspace_.options.renderer,rendererOverrides:this.workspace_.options.rendererOverrides});if(this.workspace_.horizontalLayout){a.toolboxPosition=this.workspace_.toolboxPosition==Blockly.TOOLBOX_AT_TOP?Blockly.TOOLBOX_AT_BOTTOM: Blockly.TOOLBOX_AT_TOP;if(!Blockly.HorizontalFlyout)throw Error("Missing require for Blockly.HorizontalFlyout");this.flyout=new Blockly.HorizontalFlyout(a)}else{a.toolboxPosition=this.workspace_.toolboxPosition==Blockly.TOOLBOX_AT_RIGHT?Blockly.TOOLBOX_AT_LEFT:Blockly.TOOLBOX_AT_RIGHT;if(!Blockly.VerticalFlyout)throw Error("Missing require for Blockly.VerticalFlyout");this.flyout=new Blockly.VerticalFlyout(a)}this.workspace_.addChangeListener(this.onDelete_.bind(this))}}; Blockly.Trashcan.prototype.WIDTH_=47;Blockly.Trashcan.prototype.BODY_HEIGHT_=44;Blockly.Trashcan.prototype.LID_HEIGHT_=16;Blockly.Trashcan.prototype.MARGIN_BOTTOM_=20;Blockly.Trashcan.prototype.MARGIN_SIDE_=20;Blockly.Trashcan.prototype.MARGIN_HOTSPOT_=10;Blockly.Trashcan.prototype.SPRITE_LEFT_=0;Blockly.Trashcan.prototype.SPRITE_TOP_=32;Blockly.Trashcan.prototype.HAS_BLOCKS_LID_ANGLE_=.1;Blockly.Trashcan.ANIMATION_LENGTH_=80;Blockly.Trashcan.ANIMATION_FRAMES_=4;Blockly.Trashcan.OPACITY_MIN_=.4; Blockly.Trashcan.OPACITY_MAX_=.8;Blockly.Trashcan.MAX_LID_ANGLE_=45;Blockly.Trashcan.prototype.isOpen=!1;Blockly.Trashcan.prototype.minOpenness_=0;Blockly.Trashcan.prototype.svgGroup_=null;Blockly.Trashcan.prototype.svgLid_=null;Blockly.Trashcan.prototype.lidTask_=0;Blockly.Trashcan.prototype.lidOpen_=0;Blockly.Trashcan.prototype.left_=0;Blockly.Trashcan.prototype.top_=0; @@ -962,19 +979,19 @@ Blockly.Css.register([".blocklyZoom>image, .blocklyZoom>svg>image {","opacity: . Blockly.Mutator.prototype.drawIcon_=function(a){Blockly.utils.dom.createSvgElement("rect",{"class":"blocklyIconShape",rx:"4",ry:"4",height:"16",width:"16"},a);Blockly.utils.dom.createSvgElement("path",{"class":"blocklyIconSymbol",d:"m4.203,7.296 0,1.368 -0.92,0.677 -0.11,0.41 0.9,1.559 0.41,0.11 1.043,-0.457 1.187,0.683 0.127,1.134 0.3,0.3 1.8,0 0.3,-0.299 0.127,-1.138 1.185,-0.682 1.046,0.458 0.409,-0.11 0.9,-1.559 -0.11,-0.41 -0.92,-0.677 0,-1.366 0.92,-0.677 0.11,-0.41 -0.9,-1.559 -0.409,-0.109 -1.046,0.458 -1.185,-0.682 -0.127,-1.138 -0.3,-0.299 -1.8,0 -0.3,0.3 -0.126,1.135 -1.187,0.682 -1.043,-0.457 -0.41,0.11 -0.899,1.559 0.108,0.409z"}, a);Blockly.utils.dom.createSvgElement("circle",{"class":"blocklyIconShape",r:"2.7",cx:"8",cy:"8"},a)};Blockly.Mutator.prototype.iconClick_=function(a){this.block_.isEditable()&&Blockly.Icon.prototype.iconClick_.call(this,a)}; Blockly.Mutator.prototype.createEditor_=function(){this.svgDialog_=Blockly.utils.dom.createSvgElement("svg",{x:Blockly.Bubble.BORDER_WIDTH,y:Blockly.Bubble.BORDER_WIDTH},null);if(this.quarkNames_.length)for(var a=Blockly.utils.xml.createElement("xml"),b=0,c;c=this.quarkNames_[b];b++){var d=Blockly.utils.xml.createElement("block");d.setAttribute("type",c);a.appendChild(d)}else a=null;b=new Blockly.Options({disable:!1,parentWorkspace:this.block_.workspace,media:this.block_.workspace.options.pathToMedia, -rtl:this.block_.RTL,horizontalLayout:!1,renderer:this.block_.workspace.options.renderer,rendererOverrides:this.block_.workspace.options.rendererOverrides});b.toolboxPosition=this.block_.RTL?Blockly.TOOLBOX_AT_RIGHT:Blockly.TOOLBOX_AT_LEFT;b.languageTree=a;b.getMetrics=this.getFlyoutMetrics_.bind(this);this.workspace_=new Blockly.WorkspaceSvg(b);this.workspace_.isMutator=!0;this.workspace_.addChangeListener(Blockly.Events.disableOrphans);a=this.workspace_.addFlyout("g");b=this.workspace_.createDom("blocklyMutatorBackground"); -b.insertBefore(a,this.workspace_.svgBlockCanvas_);this.svgDialog_.appendChild(b);return this.svgDialog_};Blockly.Mutator.prototype.updateEditable=function(){Blockly.Mutator.superClass_.updateEditable.call(this);this.block_.isInFlyout||(this.block_.isEditable()?this.iconGroup_&&Blockly.utils.dom.removeClass(this.iconGroup_,"blocklyIconGroupReadonly"):(this.setVisible(!1),this.iconGroup_&&Blockly.utils.dom.addClass(this.iconGroup_,"blocklyIconGroupReadonly")))}; +rtl:this.block_.RTL,horizontalLayout:!1,renderer:this.block_.workspace.options.renderer,rendererOverrides:this.block_.workspace.options.rendererOverrides});b.toolboxPosition=this.block_.RTL?Blockly.TOOLBOX_AT_RIGHT:Blockly.TOOLBOX_AT_LEFT;if(c=!!a)b.languageTree=Blockly.utils.toolbox.convertToolboxToJSON(a),b.getMetrics=this.getFlyoutMetrics_.bind(this);this.workspace_=new Blockly.WorkspaceSvg(b);this.workspace_.isMutator=!0;this.workspace_.addChangeListener(Blockly.Events.disableOrphans);a=c?this.workspace_.addFlyout("g"): +null;b=this.workspace_.createDom("blocklyMutatorBackground");a&&b.insertBefore(a,this.workspace_.svgBlockCanvas_);this.svgDialog_.appendChild(b);return this.svgDialog_};Blockly.Mutator.prototype.updateEditable=function(){Blockly.Mutator.superClass_.updateEditable.call(this);this.block_.isInFlyout||(this.block_.isEditable()?this.iconGroup_&&Blockly.utils.dom.removeClass(this.iconGroup_,"blocklyIconGroupReadonly"):(this.setVisible(!1),this.iconGroup_&&Blockly.utils.dom.addClass(this.iconGroup_,"blocklyIconGroupReadonly")))}; Blockly.Mutator.prototype.resizeBubble_=function(){var a=2*Blockly.Bubble.BORDER_WIDTH,b=this.workspace_.getCanvas().getBBox();var c=this.block_.RTL?-b.x:b.width+b.x;b=b.height+3*a;var d=this.workspace_.getFlyout();d&&(d=d.getMetrics_(),b=Math.max(b,d.contentHeight+20));c+=3*a;if(Math.abs(this.workspaceWidth_-c)>a||Math.abs(this.workspaceHeight_-b)>a)this.workspaceWidth_=c,this.workspaceHeight_=b,this.bubble_.setBubbleSize(c+a,b+a),this.svgDialog_.setAttribute("width",this.workspaceWidth_),this.svgDialog_.setAttribute("height", this.workspaceHeight_);this.block_.RTL&&(a="translate("+this.workspaceWidth_+",0)",this.workspace_.getCanvas().setAttribute("transform",a));this.workspace_.resize()};Blockly.Mutator.prototype.onBubbleMove_=function(){this.workspace_&&this.workspace_.recordDeleteAreas()}; -Blockly.Mutator.prototype.setVisible=function(a){if(a!=this.isVisible())if(Blockly.Events.fire(new Blockly.Events.Ui(this.block_,"mutatorOpen",!a,a)),a){this.bubble_=new Blockly.Bubble(this.block_.workspace,this.createEditor_(),this.block_.pathObject.svgPath,this.iconXY_,null,null);this.bubble_.setSvgId(this.block_.id);this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this));var b=this.workspace_.options.languageTree;a=this.workspace_.getFlyout();b&&(a.init(this.workspace_),a.show(b.childNodes)); -this.rootBlock_=this.block_.decompose(this.workspace_);b=this.rootBlock_.getDescendants(!1);for(var c=0,d;d=b[c];c++)d.render();this.rootBlock_.setMovable(!1);this.rootBlock_.setDeletable(!1);a?(b=2*a.CORNER_RADIUS,a=a.getWidth()+b):a=b=16;this.block_.RTL&&(a=-a);this.rootBlock_.moveBy(a,b);if(this.block_.saveConnections){var e=this,f=this.block_;f.saveConnections(this.rootBlock_);this.sourceListener_=function(){f.saveConnections(e.rootBlock_)};this.block_.workspace.addChangeListener(this.sourceListener_)}this.resizeBubble_(); +Blockly.Mutator.prototype.setVisible=function(a){if(a!=this.isVisible())if(Blockly.Events.fire(new Blockly.Events.Ui(this.block_,"mutatorOpen",!a,a)),a){this.bubble_=new Blockly.Bubble(this.block_.workspace,this.createEditor_(),this.block_.pathObject.svgPath,this.iconXY_,null,null);this.bubble_.setSvgId(this.block_.id);this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this));var b=this.workspace_.options.languageTree;a=this.workspace_.getFlyout();b&&(a.init(this.workspace_),a.show(b));this.rootBlock_= +this.block_.decompose(this.workspace_);b=this.rootBlock_.getDescendants(!1);for(var c=0,d;d=b[c];c++)d.render();this.rootBlock_.setMovable(!1);this.rootBlock_.setDeletable(!1);a?(b=2*a.CORNER_RADIUS,a=a.getWidth()+b):a=b=16;this.block_.RTL&&(a=-a);this.rootBlock_.moveBy(a,b);if(this.block_.saveConnections){var e=this,f=this.block_;f.saveConnections(this.rootBlock_);this.sourceListener_=function(){f.saveConnections(e.rootBlock_)};this.block_.workspace.addChangeListener(this.sourceListener_)}this.resizeBubble_(); this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));this.applyColour()}else this.svgDialog_=null,this.workspace_.dispose(),this.rootBlock_=this.workspace_=null,this.bubble_.dispose(),this.bubble_=null,this.workspaceHeight_=this.workspaceWidth_=0,this.sourceListener_&&(this.block_.workspace.removeChangeListener(this.sourceListener_),this.sourceListener_=null)}; -Blockly.Mutator.prototype.workspaceChanged_=function(a){if(a.type!=Blockly.Events.UI&&(a.type!=Blockly.Events.CHANGE||"disabled"!=a.element)){if(!this.workspace_.isDragging()){a=this.workspace_.getTopBlocks(!1);for(var b=0,c;c=a[b];b++){var d=c.getRelativeToSurfaceXY(),e=c.getHeightWidth();20>d.y+e.height&&c.moveBy(0,20-e.height-d.y)}}if(this.rootBlock_.workspace==this.workspace_){Blockly.Events.setGroup(!0);c=this.block_;a=(a=c.mutationToDom())&&Blockly.Xml.domToText(a);c.compose(this.rootBlock_); -c.initSvg();c.render();Blockly.getMainWorkspace().keyboardAccessibilityMode&&Blockly.navigation.moveCursorOnBlockMutation(c);b=(b=c.mutationToDom())&&Blockly.Xml.domToText(b);if(a!=b){Blockly.Events.fire(new Blockly.Events.BlockChange(c,"mutation",null,a,b));var f=Blockly.Events.getGroup();setTimeout(function(){Blockly.Events.setGroup(f);c.bumpNeighbours();Blockly.Events.setGroup(!1)},Blockly.BUMP_DELAY)}this.workspace_.isDragging()||this.resizeBubble_();Blockly.Events.setGroup(!1)}}}; -Blockly.Mutator.prototype.getFlyoutMetrics_=function(){return{viewHeight:this.workspaceHeight_,viewWidth:this.workspaceWidth_-this.workspace_.getFlyout().getWidth(),absoluteTop:0,absoluteLeft:this.workspace_.RTL?0:this.workspace_.getFlyout().getWidth()}};Blockly.Mutator.prototype.dispose=function(){this.block_.mutator=null;Blockly.Icon.prototype.dispose.call(this)}; +Blockly.Mutator.prototype.workspaceChanged_=function(a){if(a.type!=Blockly.Events.UI&&(a.type!=Blockly.Events.CHANGE||"disabled"!=a.element)){if(!this.workspace_.isDragging())for(var b=this.workspace_.getTopBlocks(!1),c=0;a=b[c];c++){var d=a.getRelativeToSurfaceXY(),e=a.getHeightWidth();20>d.y+e.height&&a.moveBy(0,20-e.height-d.y)}this.rootBlock_.workspace==this.workspace_&&(Blockly.Events.setGroup(!0),a=this.block_,b=(b=a.mutationToDom())&&Blockly.Xml.domToText(b),a.compose(this.rootBlock_),a.initSvg(), +a.render(),Blockly.getMainWorkspace().keyboardAccessibilityMode&&Blockly.navigation.moveCursorOnBlockMutation(a),c=(c=a.mutationToDom())&&Blockly.Xml.domToText(c),b!=c&&Blockly.Events.fire(new Blockly.Events.BlockChange(a,"mutation",null,b,c)),this.workspace_.isDragging()||this.resizeBubble_(),Blockly.Events.setGroup(!1))}}; +Blockly.Mutator.prototype.getFlyoutMetrics_=function(){return{contentHeight:0,contentWidth:0,contentTop:0,contentLeft:0,viewHeight:this.workspaceHeight_,viewWidth:this.workspaceWidth_-this.workspace_.getFlyout().getWidth(),viewTop:0,viewLeft:0,absoluteTop:0,absoluteLeft:this.workspace_.RTL?0:this.workspace_.getFlyout().getWidth()}};Blockly.Mutator.prototype.dispose=function(){this.block_.mutator=null;Blockly.Icon.prototype.dispose.call(this)}; Blockly.Mutator.prototype.updateBlockStyle=function(){var a=this.workspace_;if(a&&a.getAllBlocks(!1)){for(var b=a.getAllBlocks(!1),c=0;c=a&&this.sourceBlock_.outputConnection&&!b}else this.fullBlockClickTarget_=!1;this.fullBlockClickTarget_?this.clickTarget_=this.sourceBlock_.getSvgRoot():this.createBorderRect_();this.createTextElement_()}; Blockly.FieldTextInput.prototype.doClassValidation_=function(a){return null===a||void 0===a?null:String(a)};Blockly.FieldTextInput.prototype.doValueInvalid_=function(a){this.isBeingEdited_&&(this.isTextValid_=!1,a=this.value_,this.value_=this.htmlInput_.untypedDefaultValue_,this.sourceBlock_&&Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockChange(this.sourceBlock_,"field",this.name||null,a,this.value_)))}; Blockly.FieldTextInput.prototype.doValueUpdate_=function(a){this.isTextValid_=!0;this.value_=a;this.isBeingEdited_||(this.isDirty_=!0)};Blockly.FieldTextInput.prototype.applyColour=function(){this.sourceBlock_&&this.getConstants().FULL_BLOCK_FIELDS&&(this.borderRect_?this.borderRect_.setAttribute("stroke",this.sourceBlock_.style.colourTertiary):this.sourceBlock_.pathObject.svgPath.setAttribute("fill",this.getConstants().FIELD_BORDER_RECT_COLOUR))}; @@ -990,7 +1007,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_=function(a){a.keyCode==Bloc Blockly.FieldTextInput.prototype.onHtmlInputChange_=function(a){a=this.htmlInput_.value;a!==this.htmlInput_.oldValue_&&(this.htmlInput_.oldValue_=a,Blockly.Events.setGroup(!0),a=this.getValueFromEditorText_(a),this.setValue(a),this.forceRerender(),this.resizeEditor_(),Blockly.Events.setGroup(!1))};Blockly.FieldTextInput.prototype.setEditorValue_=function(a){this.isDirty_=!0;this.isBeingEdited_&&(this.htmlInput_.value=this.getEditorText_(a));this.setValue(a)}; Blockly.FieldTextInput.prototype.resizeEditor_=function(){var a=Blockly.WidgetDiv.DIV,b=this.getScaledBBox();a.style.width=b.right-b.left+"px";a.style.height=b.bottom-b.top+"px";b=new Blockly.utils.Coordinate(this.sourceBlock_.RTL?b.right-a.offsetWidth:b.left,b.top);a.style.left=b.x+"px";a.style.top=b.y+"px"}; Blockly.FieldTextInput.numberValidator=function(a){console.warn("Blockly.FieldTextInput.numberValidator is deprecated. Use Blockly.FieldNumber instead.");if(null===a)return null;a=String(a);a=a.replace(/O/ig,"0");a=a.replace(/,/g,"");a=Number(a||0);return isNaN(a)?null:String(a)};Blockly.FieldTextInput.nonnegativeIntegerValidator=function(a){(a=Blockly.FieldTextInput.numberValidator(a))&&(a=String(Math.max(0,Math.floor(a))));return a};Blockly.FieldTextInput.prototype.isTabNavigable=function(){return!0}; -Blockly.FieldTextInput.prototype.getText_=function(){return this.isBeingEdited_&&this.htmlInput_?this.htmlInput_.value:null};Blockly.FieldTextInput.prototype.getEditorText_=function(a){return String(a)};Blockly.FieldTextInput.prototype.getValueFromEditorText_=function(a){return a};Blockly.fieldRegistry.register("field_input",Blockly.FieldTextInput);Blockly.FieldAngle=function(a,b,c){this.clockwise_=Blockly.FieldAngle.CLOCKWISE;this.offset_=Blockly.FieldAngle.OFFSET;this.wrap_=Blockly.FieldAngle.WRAP;this.round_=Blockly.FieldAngle.ROUND;Blockly.FieldAngle.superClass_.constructor.call(this,a||0,b,c);this.moveSurfaceWrapper_=this.clickSurfaceWrapper_=this.clickWrapper_=this.line_=this.gauge_=null};Blockly.utils.object.inherits(Blockly.FieldAngle,Blockly.FieldTextInput); +Blockly.FieldTextInput.prototype.getText_=function(){return this.isBeingEdited_&&this.htmlInput_?this.htmlInput_.value:null};Blockly.FieldTextInput.prototype.getEditorText_=function(a){return String(a)};Blockly.FieldTextInput.prototype.getValueFromEditorText_=function(a){return a};Blockly.fieldRegistry.register("field_input",Blockly.FieldTextInput);Blockly.FieldAngle=function(a,b,c){this.clockwise_=Blockly.FieldAngle.CLOCKWISE;this.offset_=Blockly.FieldAngle.OFFSET;this.wrap_=Blockly.FieldAngle.WRAP;this.round_=Blockly.FieldAngle.ROUND;Blockly.FieldAngle.superClass_.constructor.call(this,a,b,c);this.moveSurfaceWrapper_=this.clickSurfaceWrapper_=this.clickWrapper_=this.line_=this.gauge_=null};Blockly.utils.object.inherits(Blockly.FieldAngle,Blockly.FieldTextInput);Blockly.FieldAngle.prototype.DEFAULT_VALUE=0; Blockly.FieldAngle.fromJson=function(a){return new Blockly.FieldAngle(a.angle,void 0,a)};Blockly.FieldAngle.prototype.SERIALIZABLE=!0;Blockly.FieldAngle.ROUND=15;Blockly.FieldAngle.HALF=50;Blockly.FieldAngle.CLOCKWISE=!1;Blockly.FieldAngle.OFFSET=0;Blockly.FieldAngle.WRAP=360;Blockly.FieldAngle.RADIUS=Blockly.FieldAngle.HALF-1; Blockly.FieldAngle.prototype.configure_=function(a){Blockly.FieldAngle.superClass_.configure_.call(this,a);switch(a.mode){case "compass":this.clockwise_=!0;this.offset_=90;break;case "protractor":this.clockwise_=!1,this.offset_=0}var b=a.clockwise;"boolean"==typeof b&&(this.clockwise_=b);b=a.offset;null!=b&&(b=Number(b),isNaN(b)||(this.offset_=b));b=a.wrap;null!=b&&(b=Number(b),isNaN(b)||(this.wrap_=b));a=a.round;null!=a&&(a=Number(a),isNaN(a)||(this.round_=a))}; Blockly.FieldAngle.prototype.initView=function(){Blockly.FieldAngle.superClass_.initView.call(this);this.symbol_=Blockly.utils.dom.createSvgElement("tspan",{},null);this.symbol_.appendChild(document.createTextNode("\u00b0"));this.textElement_.appendChild(this.symbol_)};Blockly.FieldAngle.prototype.render_=function(){Blockly.FieldAngle.superClass_.render_.call(this);this.updateGraph_()}; @@ -1005,17 +1022,17 @@ Blockly.FieldAngle.prototype.updateGraph_=function(){if(this.gauge_){var a=Numbe Blockly.FieldAngle.RADIUS;b=Math.abs(Math.floor((b-f)/Math.PI)%2);e&&(b=1-b);a.push(" l ",g,",",h," A ",Blockly.FieldAngle.RADIUS,",",Blockly.FieldAngle.RADIUS," 0 ",b," ",e," ",c,",",d," z")}this.gauge_.setAttribute("d",a.join(""));this.line_.setAttribute("x2",c);this.line_.setAttribute("y2",d)}}; Blockly.FieldAngle.prototype.onHtmlInputKeyDown_=function(a){Blockly.FieldAngle.superClass_.onHtmlInputKeyDown_.call(this,a);var b;a.keyCode===Blockly.utils.KeyCodes.LEFT?b=this.sourceBlock_.RTL?1:-1:a.keyCode===Blockly.utils.KeyCodes.RIGHT?b=this.sourceBlock_.RTL?-1:1:a.keyCode===Blockly.utils.KeyCodes.DOWN?b=-1:a.keyCode===Blockly.utils.KeyCodes.UP&&(b=1);if(b){var c=this.getValue();this.displayMouseOrKeyboardValue_(c+b*this.round_);a.preventDefault();a.stopPropagation()}}; Blockly.FieldAngle.prototype.doClassValidation_=function(a){a=Number(a);return isNaN(a)||!isFinite(a)?null:this.wrapValue_(a)};Blockly.FieldAngle.prototype.wrapValue_=function(a){a%=360;0>a&&(a+=360);a>this.wrap_&&(a-=360);return a};Blockly.Css.register(".blocklyAngleCircle {,stroke: #444;,stroke-width: 1;,fill: #ddd;,fill-opacity: .8;,},.blocklyAngleMarks {,stroke: #444;,stroke-width: 1;,},.blocklyAngleGauge {,fill: #f88;,fill-opacity: .8;,pointer-events: none;,},.blocklyAngleLine {,stroke: #f00;,stroke-width: 2;,stroke-linecap: round;,pointer-events: none;,}".split(",")); -Blockly.fieldRegistry.register("field_angle",Blockly.FieldAngle);Blockly.FieldCheckbox=function(a,b,c){this.checkChar_=null;null==a&&(a="FALSE");Blockly.FieldCheckbox.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldCheckbox,Blockly.Field);Blockly.FieldCheckbox.fromJson=function(a){return new Blockly.FieldCheckbox(a.checked,void 0,a)};Blockly.FieldCheckbox.CHECK_CHAR="\u2713";Blockly.FieldCheckbox.prototype.SERIALIZABLE=!0;Blockly.FieldCheckbox.prototype.CURSOR="default"; +Blockly.fieldRegistry.register("field_angle",Blockly.FieldAngle);Blockly.FieldCheckbox=function(a,b,c){this.checkChar_=null;Blockly.FieldCheckbox.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldCheckbox,Blockly.Field);Blockly.FieldCheckbox.prototype.DEFAULT_VALUE=!1;Blockly.FieldCheckbox.fromJson=function(a){return new Blockly.FieldCheckbox(a.checked,void 0,a)};Blockly.FieldCheckbox.CHECK_CHAR="\u2713";Blockly.FieldCheckbox.prototype.SERIALIZABLE=!0;Blockly.FieldCheckbox.prototype.CURSOR="default"; Blockly.FieldCheckbox.prototype.configure_=function(a){Blockly.FieldCheckbox.superClass_.configure_.call(this,a);a.checkCharacter&&(this.checkChar_=a.checkCharacter)};Blockly.FieldCheckbox.prototype.initView=function(){Blockly.FieldCheckbox.superClass_.initView.call(this);Blockly.utils.dom.addClass(this.textElement_,"blocklyCheckbox");this.textElement_.style.display=this.value_?"block":"none"}; Blockly.FieldCheckbox.prototype.render_=function(){this.textContent_&&(this.textContent_.nodeValue=this.getDisplayText_());this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET)};Blockly.FieldCheckbox.prototype.getDisplayText_=function(){return this.checkChar_||Blockly.FieldCheckbox.CHECK_CHAR};Blockly.FieldCheckbox.prototype.setCheckCharacter=function(a){this.checkChar_=a;this.forceRerender()};Blockly.FieldCheckbox.prototype.showEditor_=function(){this.setValue(!this.value_)}; Blockly.FieldCheckbox.prototype.doClassValidation_=function(a){return!0===a||"TRUE"===a?"TRUE":!1===a||"FALSE"===a?"FALSE":null};Blockly.FieldCheckbox.prototype.doValueUpdate_=function(a){this.value_=this.convertValueToBool_(a);this.textElement_&&(this.textElement_.style.display=this.value_?"block":"none")};Blockly.FieldCheckbox.prototype.getValue=function(){return this.value_?"TRUE":"FALSE"};Blockly.FieldCheckbox.prototype.getValueBoolean=function(){return this.value_}; -Blockly.FieldCheckbox.prototype.getText=function(){return String(this.convertValueToBool_(this.value_))};Blockly.FieldCheckbox.prototype.convertValueToBool_=function(a){return"string"==typeof a?"TRUE"==a:!!a};Blockly.fieldRegistry.register("field_checkbox",Blockly.FieldCheckbox);Blockly.FieldColour=function(a,b,c){Blockly.FieldColour.superClass_.constructor.call(this,a||Blockly.FieldColour.COLOURS[0],b,c);this.onKeyDownWrapper_=this.onMouseLeaveWrapper_=this.onMouseEnterWrapper_=this.onMouseMoveWrapper_=this.onClickWrapper_=this.highlightedIndex_=this.picker_=null};Blockly.utils.object.inherits(Blockly.FieldColour,Blockly.Field);Blockly.FieldColour.fromJson=function(a){return new Blockly.FieldColour(a.colour,void 0,a)};Blockly.FieldColour.prototype.SERIALIZABLE=!0; -Blockly.FieldColour.prototype.CURSOR="default";Blockly.FieldColour.prototype.isDirty_=!1;Blockly.FieldColour.prototype.colours_=null;Blockly.FieldColour.prototype.titles_=null;Blockly.FieldColour.prototype.columns_=0;Blockly.FieldColour.prototype.configure_=function(a){Blockly.FieldColour.superClass_.configure_.call(this,a);a.colourOptions&&(this.colours_=a.colourOptions,this.titles_=a.colourTitles);a.columns&&(this.columns_=a.columns)}; +Blockly.FieldCheckbox.prototype.getText=function(){return String(this.convertValueToBool_(this.value_))};Blockly.FieldCheckbox.prototype.convertValueToBool_=function(a){return"string"==typeof a?"TRUE"==a:!!a};Blockly.fieldRegistry.register("field_checkbox",Blockly.FieldCheckbox);Blockly.FieldColour=function(a,b,c){Blockly.FieldColour.superClass_.constructor.call(this,a,b,c);this.onKeyDownWrapper_=this.onMouseLeaveWrapper_=this.onMouseEnterWrapper_=this.onMouseMoveWrapper_=this.onClickWrapper_=this.highlightedIndex_=this.picker_=null};Blockly.utils.object.inherits(Blockly.FieldColour,Blockly.Field);Blockly.FieldColour.fromJson=function(a){return new Blockly.FieldColour(a.colour,void 0,a)};Blockly.FieldColour.prototype.SERIALIZABLE=!0;Blockly.FieldColour.prototype.CURSOR="default"; +Blockly.FieldColour.prototype.isDirty_=!1;Blockly.FieldColour.prototype.colours_=null;Blockly.FieldColour.prototype.titles_=null;Blockly.FieldColour.prototype.columns_=0;Blockly.FieldColour.prototype.configure_=function(a){Blockly.FieldColour.superClass_.configure_.call(this,a);a.colourOptions&&(this.colours_=a.colourOptions,this.titles_=a.colourTitles);a.columns&&(this.columns_=a.columns)}; Blockly.FieldColour.prototype.initView=function(){this.size_=new Blockly.utils.Size(this.getConstants().FIELD_COLOUR_DEFAULT_WIDTH,this.getConstants().FIELD_COLOUR_DEFAULT_HEIGHT);this.getConstants().FIELD_COLOUR_FULL_BLOCK?this.clickTarget_=this.sourceBlock_.getSvgRoot():(this.createBorderRect_(),this.borderRect_.style.fillOpacity="1")}; Blockly.FieldColour.prototype.applyColour=function(){this.getConstants().FIELD_COLOUR_FULL_BLOCK?(this.sourceBlock_.pathObject.svgPath.setAttribute("fill",this.getValue()),this.sourceBlock_.pathObject.svgPath.setAttribute("stroke","#fff")):this.borderRect_&&(this.borderRect_.style.fill=this.getValue())};Blockly.FieldColour.prototype.doClassValidation_=function(a){return"string"!=typeof a?null:Blockly.utils.colour.parse(a)}; Blockly.FieldColour.prototype.doValueUpdate_=function(a){this.value_=a;this.borderRect_?this.borderRect_.style.fill=a:this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.pathObject.svgPath.setAttribute("fill",a),this.sourceBlock_.pathObject.svgPath.setAttribute("stroke","#fff"))};Blockly.FieldColour.prototype.getText=function(){var a=this.value_;/^#(.)\1(.)\2(.)\3$/.test(a)&&(a="#"+a[1]+a[3]+a[5]);return a};Blockly.FieldColour.COLOURS="#ffffff #cccccc #c0c0c0 #999999 #666666 #333333 #000000 #ffcccc #ff6666 #ff0000 #cc0000 #990000 #660000 #330000 #ffcc99 #ff9966 #ff9900 #ff6600 #cc6600 #993300 #663300 #ffff99 #ffff66 #ffcc66 #ffcc33 #cc9933 #996633 #663333 #ffffcc #ffff33 #ffff00 #ffcc00 #999900 #666600 #333300 #99ff99 #66ff99 #33ff33 #33cc00 #009900 #006600 #003300 #99ffff #33ffff #66cccc #00cccc #339999 #336666 #003333 #ccffff #66ffff #33ccff #3366ff #3333ff #000099 #000066 #ccccff #9999ff #6666cc #6633ff #6600cc #333399 #330099 #ffccff #ff99ff #cc66cc #cc33cc #993399 #663366 #330033".split(" "); -Blockly.FieldColour.TITLES=[];Blockly.FieldColour.COLUMNS=7;Blockly.FieldColour.prototype.setColours=function(a,b){this.colours_=a;b&&(this.titles_=b);return this};Blockly.FieldColour.prototype.setColumns=function(a){this.columns_=a;return this};Blockly.FieldColour.prototype.showEditor_=function(){this.picker_=this.dropdownCreate_();Blockly.DropDownDiv.getContentDiv().appendChild(this.picker_);Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.picker_.focus({preventScroll:!0})}; -Blockly.FieldColour.prototype.onClick_=function(a){a=(a=a.target)&&a.label;null!==a&&(this.setValue(a),Blockly.DropDownDiv.hideIfOwner(this))}; +Blockly.FieldColour.prototype.DEFAULT_VALUE=Blockly.FieldColour.COLOURS[0];Blockly.FieldColour.TITLES=[];Blockly.FieldColour.COLUMNS=7;Blockly.FieldColour.prototype.setColours=function(a,b){this.colours_=a;b&&(this.titles_=b);return this};Blockly.FieldColour.prototype.setColumns=function(a){this.columns_=a;return this}; +Blockly.FieldColour.prototype.showEditor_=function(){this.picker_=this.dropdownCreate_();Blockly.DropDownDiv.getContentDiv().appendChild(this.picker_);Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.picker_.focus({preventScroll:!0})};Blockly.FieldColour.prototype.onClick_=function(a){a=(a=a.target)&&a.label;null!==a&&(this.setValue(a),Blockly.DropDownDiv.hideIfOwner(this))}; Blockly.FieldColour.prototype.onKeyDown_=function(a){var b=!1;if(a.keyCode===Blockly.utils.KeyCodes.UP)this.moveHighlightBy_(0,-1),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.DOWN)this.moveHighlightBy_(0,1),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.LEFT)this.moveHighlightBy_(-1,0),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.RIGHT)this.moveHighlightBy_(1,0),b=!0;else if(a.keyCode===Blockly.utils.KeyCodes.ENTER){if(b=this.getHighlighted_())b=b&&b.label,null!==b&&this.setValue(b);Blockly.DropDownDiv.hideWithoutAnimation(); b=!0}b&&a.stopPropagation()};Blockly.FieldColour.prototype.onBlocklyAction=function(a){if(this.picker_){if(a===Blockly.navigation.ACTION_PREVIOUS)return this.moveHighlightBy_(0,-1),!0;if(a===Blockly.navigation.ACTION_NEXT)return this.moveHighlightBy_(0,1),!0;if(a===Blockly.navigation.ACTION_OUT)return this.moveHighlightBy_(-1,0),!0;if(a===Blockly.navigation.ACTION_IN)return this.moveHighlightBy_(1,0),!0}return Blockly.FieldColour.superClass_.onBlocklyAction.call(this,a)}; Blockly.FieldColour.prototype.moveHighlightBy_=function(a,b){var c=this.colours_||Blockly.FieldColour.COLOURS,d=this.columns_||Blockly.FieldColour.COLUMNS,e=this.highlightedIndex_%d,f=Math.floor(this.highlightedIndex_/d);e+=a;f+=b;0>a?0>e&&0e&&(e=0):0d-1&&fd-1&&e--:0>b?0>f&&(f=0):0Math.floor(c.length/d)-1&&(f=Math.floor(c.length/d)-1);this.setHighlightedCell_(this.picker_.childNodes[f].childNodes[e],f*d+e)}; @@ -1029,19 +1046,18 @@ b[g]);Blockly.utils.aria.setState(h,Blockly.utils.aria.State.SELECTED,b[g]==d);h Blockly.FieldColour.prototype.dropdownDispose_=function(){this.onClickWrapper_&&(Blockly.unbindEvent_(this.onClickWrapper_),this.onClickWrapper_=null);this.onMouseMoveWrapper_&&(Blockly.unbindEvent_(this.onMouseMoveWrapper_),this.onMouseMoveWrapper_=null);this.onMouseEnterWrapper_&&(Blockly.unbindEvent_(this.onMouseEnterWrapper_),this.onMouseEnterWrapper_=null);this.onMouseLeaveWrapper_&&(Blockly.unbindEvent_(this.onMouseLeaveWrapper_),this.onMouseLeaveWrapper_=null);this.onKeyDownWrapper_&&(Blockly.unbindEvent_(this.onKeyDownWrapper_), this.onKeyDownWrapper_=null);this.highlightedIndex_=this.picker_=null}; Blockly.Css.register([".blocklyColourTable {","border-collapse: collapse;","display: block;","outline: none;","padding: 1px;","}",".blocklyColourTable>tr>td {","border: .5px solid #888;","box-sizing: border-box;","cursor: pointer;","display: inline-block;","height: 20px;","padding: 0;","width: 20px;","}",".blocklyColourTable>tr>td.blocklyColourHighlighted {","border-color: #eee;","box-shadow: 2px 2px 7px 2px rgba(0,0,0,.3);","position: relative;","}",".blocklyColourSelected, .blocklyColourSelected:hover {", -"border-color: #eee !important;","outline: 1px solid #333;","position: relative;","}"]);Blockly.fieldRegistry.register("field_colour",Blockly.FieldColour);Blockly.FieldDropdown=function(a,b,c){"function"!=typeof a&&Blockly.FieldDropdown.validateOptions_(a);this.menuGenerator_=a;this.generatedOptions_=null;this.trimOptions_();this.selectedOption_=this.getOptions(!1)[0];Blockly.FieldDropdown.superClass_.constructor.call(this,this.selectedOption_[1],b,c);this.svgArrow_=this.arrow_=this.imageElement_=this.menu_=this.selectedMenuItem_=null};Blockly.utils.object.inherits(Blockly.FieldDropdown,Blockly.Field); +"border-color: #eee !important;","outline: 1px solid #333;","position: relative;","}"]);Blockly.fieldRegistry.register("field_colour",Blockly.FieldColour);Blockly.FieldDropdown=function(a,b,c){"function"!=typeof a&&Blockly.FieldDropdown.validateOptions_(a);this.menuGenerator_=a;this.suffixField=this.prefixField=this.generatedOptions_=null;this.trimOptions_();this.selectedOption_=this.getOptions(!1)[0];Blockly.FieldDropdown.superClass_.constructor.call(this,this.selectedOption_[1],b,c);this.svgArrow_=this.arrow_=this.imageElement_=this.menu_=this.selectedMenuItem_=null};Blockly.utils.object.inherits(Blockly.FieldDropdown,Blockly.Field); Blockly.FieldDropdown.fromJson=function(a){return new Blockly.FieldDropdown(a.options,void 0,a)};Blockly.FieldDropdown.prototype.SERIALIZABLE=!0;Blockly.FieldDropdown.CHECKMARK_OVERHANG=25;Blockly.FieldDropdown.MAX_MENU_HEIGHT_VH=.45;Blockly.FieldDropdown.IMAGE_Y_OFFSET=5;Blockly.FieldDropdown.IMAGE_Y_PADDING=2*Blockly.FieldDropdown.IMAGE_Y_OFFSET;Blockly.FieldDropdown.ARROW_CHAR=Blockly.utils.userAgent.ANDROID?"\u25bc":"\u25be";Blockly.FieldDropdown.prototype.CURSOR="default"; Blockly.FieldDropdown.prototype.initView=function(){this.shouldAddBorderRect_()?this.createBorderRect_():this.clickTarget_=this.sourceBlock_.getSvgRoot();this.createTextElement_();this.imageElement_=Blockly.utils.dom.createSvgElement("image",{},this.fieldGroup_);this.getConstants().FIELD_DROPDOWN_SVG_ARROW?this.createSVGArrow_():this.createTextArrow_();this.borderRect_&&Blockly.utils.dom.addClass(this.borderRect_,"blocklyDropdownRect")}; Blockly.FieldDropdown.prototype.shouldAddBorderRect_=function(){return!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW||this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW&&!this.sourceBlock_.isShadow()}; Blockly.FieldDropdown.prototype.createTextArrow_=function(){this.arrow_=Blockly.utils.dom.createSvgElement("tspan",{},this.textElement_);this.arrow_.appendChild(document.createTextNode(this.sourceBlock_.RTL?Blockly.FieldDropdown.ARROW_CHAR+" ":" "+Blockly.FieldDropdown.ARROW_CHAR));this.sourceBlock_.RTL?this.textElement_.insertBefore(this.arrow_,this.textContent_):this.textElement_.appendChild(this.arrow_)}; Blockly.FieldDropdown.prototype.createSVGArrow_=function(){this.svgArrow_=Blockly.utils.dom.createSvgElement("image",{height:this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE+"px",width:this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE+"px"},this.fieldGroup_);this.svgArrow_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.getConstants().FIELD_DROPDOWN_SVG_ARROW_DATAURI)}; -Blockly.FieldDropdown.prototype.showEditor_=function(a){this.menu_=this.dropdownCreate_();this.menu_.openingCoords=a&&"number"===typeof a.clientX?new Blockly.utils.Coordinate(a.clientX,a.clientY):null;this.menu_.render(Blockly.DropDownDiv.getContentDiv());Blockly.utils.dom.addClass(this.menu_.getElement(),"blocklyDropdownMenu");if(this.getConstants().FIELD_DROPDOWN_COLOURED_DIV){a=this.sourceBlock_.isShadow()?this.sourceBlock_.getParent().getColour():this.sourceBlock_.getColour();var b=this.sourceBlock_.isShadow()? -this.sourceBlock_.getParent().style.colourTertiary:this.sourceBlock_.style.colourTertiary;Blockly.DropDownDiv.setColour(a,b)}Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.menu_.focus();this.selectedMenuItem_&&Blockly.utils.style.scrollIntoContainerView(this.selectedMenuItem_.getElement(),this.menu_.getElement());this.applyColour()}; -Blockly.FieldDropdown.prototype.dropdownCreate_=function(){var a=new Blockly.Menu;a.setRightToLeft(this.sourceBlock_.RTL);a.setRole(Blockly.utils.aria.Role.LISTBOX);var b=this.getOptions(!1);this.selectedMenuItem_=null;for(var c=0;ca.length)){b=[];for(c=0;ca.length)){b=[];for(c=0;c=c||0>=b)throw Error("Height and width values of an image field must be greater than 0.");this.flipRtl_=!1;this.altText_="";Blockly.FieldImage.superClass_.constructor.call(this, -a||"",null,g);g||(this.flipRtl_=!!f,this.altText_=Blockly.utils.replaceMessageReferences(d)||"");this.size_=new Blockly.utils.Size(b,c+Blockly.FieldImage.Y_PADDING);this.imageHeight_=c;this.clickHandler_=null;"function"==typeof e&&(this.clickHandler_=e);this.imageElement_=null};Blockly.utils.object.inherits(Blockly.FieldImage,Blockly.Field);Blockly.FieldImage.fromJson=function(a){return new Blockly.FieldImage(a.src,a.width,a.height,void 0,void 0,void 0,a)};Blockly.FieldImage.Y_PADDING=1; -Blockly.FieldImage.prototype.EDITABLE=!1;Blockly.FieldImage.prototype.isDirty_=!1;Blockly.FieldImage.prototype.configure_=function(a){Blockly.FieldImage.superClass_.configure_.call(this,a);this.flipRtl_=!!a.flipRtl;this.altText_=Blockly.utils.replaceMessageReferences(a.alt)||""}; +a,null,g);g||(this.flipRtl_=!!f,this.altText_=Blockly.utils.replaceMessageReferences(d)||"");this.size_=new Blockly.utils.Size(b,c+Blockly.FieldImage.Y_PADDING);this.imageHeight_=c;this.clickHandler_=null;"function"==typeof e&&(this.clickHandler_=e);this.imageElement_=null};Blockly.utils.object.inherits(Blockly.FieldImage,Blockly.Field);Blockly.FieldImage.prototype.DEFAULT_VALUE="";Blockly.FieldImage.fromJson=function(a){return new Blockly.FieldImage(a.src,a.width,a.height,void 0,void 0,void 0,a)}; +Blockly.FieldImage.Y_PADDING=1;Blockly.FieldImage.prototype.EDITABLE=!1;Blockly.FieldImage.prototype.isDirty_=!1;Blockly.FieldImage.prototype.configure_=function(a){Blockly.FieldImage.superClass_.configure_.call(this,a);this.flipRtl_=!!a.flipRtl;this.altText_=Blockly.utils.replaceMessageReferences(a.alt)||""}; Blockly.FieldImage.prototype.initView=function(){this.imageElement_=Blockly.utils.dom.createSvgElement("image",{height:this.imageHeight_+"px",width:this.size_.width+"px",alt:this.altText_},this.fieldGroup_);this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.value_);this.clickHandler_&&(this.imageElement_.style.cursor="pointer")};Blockly.FieldImage.prototype.updateSize_=function(){}; Blockly.FieldImage.prototype.doClassValidation_=function(a){return"string"!=typeof a?null:a};Blockly.FieldImage.prototype.doValueUpdate_=function(a){this.value_=a;this.imageElement_&&this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",String(this.value_))};Blockly.FieldImage.prototype.getFlipRtl=function(){return this.flipRtl_};Blockly.FieldImage.prototype.setAlt=function(a){a!=this.altText_&&(this.altText_=a||"",this.imageElement_&&this.imageElement_.setAttribute("alt",this.altText_))}; -Blockly.FieldImage.prototype.showEditor_=function(){this.clickHandler_&&this.clickHandler_(this)};Blockly.FieldImage.prototype.setOnClickHandler=function(a){this.clickHandler_=a};Blockly.FieldImage.prototype.getText_=function(){return this.altText_};Blockly.fieldRegistry.register("field_image",Blockly.FieldImage);Blockly.FieldMultilineInput=function(a,b,c){null==a&&(a="");Blockly.FieldMultilineInput.superClass_.constructor.call(this,a,b,c);this.textGroup_=null};Blockly.utils.object.inherits(Blockly.FieldMultilineInput,Blockly.FieldTextInput);Blockly.FieldMultilineInput.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldMultilineInput(b,void 0,a)}; +Blockly.FieldImage.prototype.showEditor_=function(){this.clickHandler_&&this.clickHandler_(this)};Blockly.FieldImage.prototype.setOnClickHandler=function(a){this.clickHandler_=a};Blockly.FieldImage.prototype.getText_=function(){return this.altText_};Blockly.fieldRegistry.register("field_image",Blockly.FieldImage);Blockly.FieldMultilineInput=function(a,b,c){Blockly.FieldMultilineInput.superClass_.constructor.call(this,a,b,c);this.textGroup_=null};Blockly.utils.object.inherits(Blockly.FieldMultilineInput,Blockly.FieldTextInput);Blockly.FieldMultilineInput.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldMultilineInput(b,void 0,a)}; Blockly.FieldMultilineInput.prototype.initView=function(){this.createBorderRect_();this.textGroup_=Blockly.utils.dom.createSvgElement("g",{"class":"blocklyEditableText"},this.fieldGroup_)}; Blockly.FieldMultilineInput.prototype.getDisplayText_=function(){var a=this.value_;if(!a)return Blockly.Field.NBSP;var b=a.split("\n");a="";for(var c=0;cthis.maxDisplayLength&&(d=d.substring(0,this.maxDisplayLength-4)+"...");d=d.replace(/\s/g,Blockly.Field.NBSP);a+=d;c!==b.length-1&&(a+="\n")}this.sourceBlock_.RTL&&(a+="\u200f");return a}; Blockly.FieldMultilineInput.prototype.render_=function(){for(var a;a=this.textGroup_.firstChild;)this.textGroup_.removeChild(a);a=this.getDisplayText_().split("\n");for(var b=0,c=0;cthis.max_&&Blockly.utils.aria.setState(a,Blockly.utils.aria.State.VALUEMAX,this.max_);return a};Blockly.fieldRegistry.register("field_number",Blockly.FieldNumber);Blockly.FieldVariable=function(a,b,c,d,e){this.menuGenerator_=Blockly.FieldVariable.dropdownCreate;this.defaultVariableName=a||"";this.size_=new Blockly.utils.Size(0,0);e&&this.configure_(e);b&&this.setValidator(b);e||this.setTypes_(c,d)};Blockly.utils.object.inherits(Blockly.FieldVariable,Blockly.FieldDropdown);Blockly.FieldVariable.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.variable);return new Blockly.FieldVariable(b,void 0,void 0,void 0,a)}; -Blockly.FieldVariable.prototype.workspace_=null;Blockly.FieldVariable.prototype.SERIALIZABLE=!0;Blockly.FieldVariable.prototype.configure_=function(a){Blockly.FieldVariable.superClass_.configure_.call(this,a);this.setTypes_(a.variableTypes,a.defaultType)};Blockly.FieldVariable.prototype.initModel=function(){if(!this.variable_){var a=Blockly.Variables.getOrCreateVariablePackage(this.sourceBlock_.workspace,null,this.defaultVariableName,this.defaultType_);this.doValueUpdate_(a.getId())}}; +Blockly.FieldVariable.prototype.SERIALIZABLE=!0;Blockly.FieldVariable.prototype.configure_=function(a){Blockly.FieldVariable.superClass_.configure_.call(this,a);this.setTypes_(a.variableTypes,a.defaultType)};Blockly.FieldVariable.prototype.initModel=function(){if(!this.variable_){var a=Blockly.Variables.getOrCreateVariablePackage(this.sourceBlock_.workspace,null,this.defaultVariableName,this.defaultType_);this.doValueUpdate_(a.getId())}}; Blockly.FieldVariable.prototype.shouldAddBorderRect_=function(){return Blockly.FieldVariable.superClass_.shouldAddBorderRect_.call(this)&&(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW||"variables_get"!=this.sourceBlock_.type)}; Blockly.FieldVariable.prototype.fromXml=function(a){var b=a.getAttribute("id"),c=a.textContent,d=a.getAttribute("variabletype")||a.getAttribute("variableType")||"";b=Blockly.Variables.getOrCreateVariablePackage(this.sourceBlock_.workspace,b,c,d);if(null!=d&&d!==b.type)throw Error("Serialized variable type with id '"+b.getId()+"' had type "+b.type+", and does not match variable field that references it: "+Blockly.Xml.domToText(a)+".");this.setValue(b.getId())}; Blockly.FieldVariable.prototype.toXml=function(a){this.initModel();a.id=this.variable_.getId();a.textContent=this.variable_.name;this.variable_.type&&a.setAttribute("variabletype",this.variable_.type);return a};Blockly.FieldVariable.prototype.setSourceBlock=function(a){if(a.isShadow())throw Error("Variable fields are not allowed to exist on shadow blocks.");Blockly.FieldVariable.superClass_.setSourceBlock.call(this,a)}; @@ -1117,12 +1133,16 @@ result:"specOut"},b);Blockly.utils.dom.createSvgElement("fePointLight",{x:-5E3,y width:10,height:10},a);Blockly.utils.dom.createSvgElement("rect",{width:10,height:10,fill:"#aaa"},b);Blockly.utils.dom.createSvgElement("path",{d:"M 0 0 L 10 10 M 10 0 L 0 10",stroke:"#cc0"},b);this.disabledPatternId=b.id;this.disabledPattern_=b;Blockly.blockRendering.Debug&&(a=Blockly.utils.dom.createSvgElement("filter",{id:"blocklyDebugFilter"+this.randomIdentifier,height:"160%",width:"180%",y:"-30%",x:"-40%"},a),b=Blockly.utils.dom.createSvgElement("feComponentTransfer",{result:"outBlur"},a),Blockly.utils.dom.createSvgElement("feFuncA", {type:"table",tableValues:"0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"},b),Blockly.utils.dom.createSvgElement("feFlood",{"flood-color":"#ff0000","flood-opacity":.5,result:"outColor"},a),Blockly.utils.dom.createSvgElement("feComposite",{"in":"outColor",in2:"outBlur",operator:"in",result:"outGlow"},a),this.debugFilterId=a.id,this.debugFilter_=a)}; Blockly.blockRendering.ConstantProvider.prototype.injectCSS_=function(a,b){b=this.getCSS_(b);a="blockly-renderer-style-"+a;this.cssNode_=document.getElementById(a);var c=b.join("\n");this.cssNode_?this.cssNode_.firstChild.textContent=c:(b=document.createElement("style"),b.id=a,a=document.createTextNode(c),b.appendChild(a),document.head.insertBefore(b,document.head.firstChild),this.cssNode_=b)}; -Blockly.blockRendering.ConstantProvider.prototype.getCSS_=function(a){return[a+" .blocklyText, ",a+" .blocklyFlyoutLabelText {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-size: "+this.FIELD_TEXT_FONTSIZE+"pt;","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+";","}",a+" .blocklyText {","fill: #fff;","}",a+" .blocklyNonEditableText>rect,",a+" .blocklyEditableText>rect {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","fill-opacity: .6;","stroke: none;","}",a+" .blocklyNonEditableText>text,",a+" .blocklyEditableText>text {", -"fill: #000;","}",a+" .blocklyFlyoutLabelText {","fill: #000;","}",a+" .blocklyText.blocklyBubbleText {","fill: #000;","}",a+" .blocklyEditableText:not(.editing):hover>rect {","stroke: #fff;","stroke-width: 2;","}",a+" .blocklyHtmlInput {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+";","}",a+" .blocklySelected>.blocklyPath {","stroke: #fc3;","stroke-width: 3px;","}",a+" .blocklyHighlightedConnectionPath {","stroke: #fc3;","}",a+" .blocklyReplaceable .blocklyPath {", -"fill-opacity: .5;","}",a+" .blocklyReplaceable .blocklyPathLight,",a+" .blocklyReplaceable .blocklyPathDark {","display: none;","}",a+" .blocklyInsertionMarker>.blocklyPath {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none","}"]};Blockly.blockRendering.MarkerSvg=function(a,b,c){this.workspace_=a;this.marker_=c;this.parent_=null;this.constants_=b;this.currentMarkerSvg=null;a=this.isCursor()?this.constants_.CURSOR_COLOUR:this.constants_.MARKER_COLOUR;this.colour_=c.colour||a};Blockly.blockRendering.MarkerSvg.CURSOR_CLASS="blocklyCursor";Blockly.blockRendering.MarkerSvg.MARKER_CLASS="blocklyMarker";Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER=.75;Blockly.blockRendering.MarkerSvg.prototype.getSvgRoot=function(){return this.svgGroup_}; -Blockly.blockRendering.MarkerSvg.prototype.isCursor=function(){return"cursor"==this.marker_.type};Blockly.blockRendering.MarkerSvg.prototype.createDom=function(){var a=this.isCursor()?Blockly.blockRendering.MarkerSvg.CURSOR_CLASS:Blockly.blockRendering.MarkerSvg.MARKER_CLASS;this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{"class":a},null);this.createDomInternal_();this.applyColour_();return this.svgGroup_}; +Blockly.blockRendering.ConstantProvider.prototype.getCSS_=function(a){return[a+" .blocklyText, ",a+" .blocklyFlyoutLabelText {","font: "+this.FIELD_TEXT_FONTWEIGHT+" "+this.FIELD_TEXT_FONTSIZE+"pt "+this.FIELD_TEXT_FONTFAMILY+";","}",a+" .blocklyText {","fill: #fff;","}",a+" .blocklyNonEditableText>rect,",a+" .blocklyEditableText>rect {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","fill-opacity: .6;","stroke: none;","}",a+" .blocklyNonEditableText>text,",a+" .blocklyEditableText>text {","fill: #000;", +"}",a+" .blocklyFlyoutLabelText {","fill: #000;","}",a+" .blocklyText.blocklyBubbleText {","fill: #000;","}",a+" .blocklyEditableText:not(.editing):hover>rect {","stroke: #fff;","stroke-width: 2;","}",a+" .blocklyHtmlInput {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+";","}",a+" .blocklySelected>.blocklyPath {","stroke: #fc3;","stroke-width: 3px;","}",a+" .blocklyHighlightedConnectionPath {","stroke: #fc3;","}",a+" .blocklyReplaceable .blocklyPath {", +"fill-opacity: .5;","}",a+" .blocklyReplaceable .blocklyPathLight,",a+" .blocklyReplaceable .blocklyPathDark {","display: none;","}",a+" .blocklyInsertionMarker>.blocklyPath {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none;","}"]};Blockly.blockRendering.MarkerSvg=function(a,b,c){this.workspace_=a;this.marker_=c;this.parent_=null;this.constants_=b;this.currentMarkerSvg=null;a=this.isCursor()?this.constants_.CURSOR_COLOUR:this.constants_.MARKER_COLOUR;this.colour_=c.colour||a};Blockly.blockRendering.MarkerSvg.CURSOR_CLASS="blocklyCursor";Blockly.blockRendering.MarkerSvg.MARKER_CLASS="blocklyMarker";Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER=.75;Blockly.blockRendering.MarkerSvg.prototype.getSvgRoot=function(){return this.svgGroup_}; +Blockly.blockRendering.MarkerSvg.prototype.getMarker=function(){return this.marker_};Blockly.blockRendering.MarkerSvg.prototype.isCursor=function(){return"cursor"==this.marker_.type};Blockly.blockRendering.MarkerSvg.prototype.createDom=function(){var a=this.isCursor()?Blockly.blockRendering.MarkerSvg.CURSOR_CLASS:Blockly.blockRendering.MarkerSvg.MARKER_CLASS;this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{"class":a},null);this.createDomInternal_();return this.svgGroup_}; Blockly.blockRendering.MarkerSvg.prototype.setParent_=function(a){this.isCursor()?(this.parent_&&this.parent_.setCursorSvg(null),a.setCursorSvg(this.getSvgRoot())):(this.parent_&&this.parent_.setMarkerSvg(null),a.setMarkerSvg(this.getSvgRoot()));this.parent_=a}; -Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_=function(a){if(a){var b=a.width,c=a.height,d=c*Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER,e=this.constants_.CURSOR_BLOCK_PADDING;if(a.previousConnection){var f=this.constants_.shapeFor(a.previousConnection);this.positionPrevious_(b,e,d,f)}else a.outputConnection?(f=this.constants_.shapeFor(a.outputConnection),this.positionOutput_(b,c,f)):this.positionBlock_(b,e,d);this.setParent_(a);this.showCurrent_()}}; +Blockly.blockRendering.MarkerSvg.prototype.draw=function(a,b){if(b){this.constants_=this.workspace_.getRenderer().getConstants();var c=this.isCursor()?this.constants_.CURSOR_COLOUR:this.constants_.MARKER_COLOUR;this.colour_=this.marker_.colour||c;this.applyColour_(b);this.showAtLocation_(b);this.fireMarkerEvent_(a,b);a=this.currentMarkerSvg.childNodes[0];void 0!==a&&a.beginElement&&a.beginElement()}else this.hide()}; +Blockly.blockRendering.MarkerSvg.prototype.showAtLocation_=function(a){var b=a.getLocation();a.getType()==Blockly.ASTNode.types.BLOCK?this.showWithBlock_(a):a.getType()==Blockly.ASTNode.types.OUTPUT?this.showWithOutput_(a):b.type==Blockly.INPUT_VALUE?this.showWithInput_(a):b.type==Blockly.NEXT_STATEMENT?this.showWithNext_(a):a.getType()==Blockly.ASTNode.types.PREVIOUS?this.showWithPrevious_(a):a.getType()==Blockly.ASTNode.types.FIELD?this.showWithField_(a):a.getType()==Blockly.ASTNode.types.WORKSPACE? +this.showWithCoordinates_(a):a.getType()==Blockly.ASTNode.types.STACK&&this.showWithStack_(a)}; +Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_=function(a){a=a.getSourceBlock();var b=a.width,c=a.height,d=c*Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER,e=this.constants_.CURSOR_BLOCK_PADDING;if(a.previousConnection){var f=this.constants_.shapeFor(a.previousConnection);this.positionPrevious_(b,e,d,f)}else a.outputConnection?(f=this.constants_.shapeFor(a.outputConnection),this.positionOutput_(b,c,f)):this.positionBlock_(b,e,d);this.setParent_(a);this.showCurrent_()}; +Blockly.blockRendering.MarkerSvg.prototype.showWithBlock_=function(a){this.showWithBlockPrevOutput_(a)};Blockly.blockRendering.MarkerSvg.prototype.showWithPrevious_=function(a){this.showWithBlockPrevOutput_(a)};Blockly.blockRendering.MarkerSvg.prototype.showWithOutput_=function(a){this.showWithBlockPrevOutput_(a)}; Blockly.blockRendering.MarkerSvg.prototype.showWithCoordinates_=function(a){var b=a.getWsCoordinate();a=b.x;b=b.y;this.workspace_.RTL&&(a-=this.constants_.CURSOR_WS_WIDTH);this.positionLine_(a,b,this.constants_.CURSOR_WS_WIDTH);this.setParent_(this.workspace_);this.showCurrent_()};Blockly.blockRendering.MarkerSvg.prototype.showWithField_=function(a){a=a.getLocation();var b=a.getSize().width,c=a.getSize().height;this.positionRect_(0,0,b,c);this.setParent_(a);this.showCurrent_()}; Blockly.blockRendering.MarkerSvg.prototype.showWithInput_=function(a){a=a.getLocation();var b=a.getSourceBlock();this.positionInput_(a);this.setParent_(b);this.showCurrent_()};Blockly.blockRendering.MarkerSvg.prototype.showWithNext_=function(a){var b=a.getLocation();a=b.getSourceBlock();var c=0;b=b.getOffsetInBlock().y;var d=a.getHeightWidth().width;this.workspace_.RTL&&(c=-d);this.positionLine_(c,b,d);this.setParent_(a);this.showCurrent_()}; Blockly.blockRendering.MarkerSvg.prototype.showWithStack_=function(a){a=a.getLocation();var b=a.getHeightWidth(),c=b.width+this.constants_.CURSOR_STACK_PADDING;b=b.height+this.constants_.CURSOR_STACK_PADDING;var d=-this.constants_.CURSOR_STACK_PADDING/2,e=-this.constants_.CURSOR_STACK_PADDING/2,f=d;this.workspace_.RTL&&(f=-(c+d));this.positionRect_(f,e,c,b);this.setParent_(a);this.showCurrent_()}; @@ -1132,16 +1152,13 @@ Blockly.blockRendering.MarkerSvg.prototype.positionLine_=function(a,b,c){this.ma Blockly.blockRendering.MarkerSvg.prototype.positionOutput_=function(a,b,c){a=Blockly.utils.svgPaths.moveBy(a,0)+Blockly.utils.svgPaths.lineOnAxis("h",-(a-c.width))+Blockly.utils.svgPaths.lineOnAxis("v",this.constants_.TAB_OFFSET_FROM_TOP)+c.pathDown+Blockly.utils.svgPaths.lineOnAxis("V",b)+Blockly.utils.svgPaths.lineOnAxis("H",a);this.markerBlock_.setAttribute("d",a);this.workspace_.RTL&&this.flipRtl_(this.markerBlock_);this.currentMarkerSvg=this.markerBlock_}; Blockly.blockRendering.MarkerSvg.prototype.positionPrevious_=function(a,b,c,d){a=Blockly.utils.svgPaths.moveBy(-b,c)+Blockly.utils.svgPaths.lineOnAxis("V",-b)+Blockly.utils.svgPaths.lineOnAxis("H",this.constants_.NOTCH_OFFSET_LEFT)+d.pathLeft+Blockly.utils.svgPaths.lineOnAxis("H",a+2*b)+Blockly.utils.svgPaths.lineOnAxis("V",c);this.markerBlock_.setAttribute("d",a);this.workspace_.RTL&&this.flipRtl_(this.markerBlock_);this.currentMarkerSvg=this.markerBlock_}; Blockly.blockRendering.MarkerSvg.prototype.positionRect_=function(a,b,c,d){this.markerSvgRect_.setAttribute("x",a);this.markerSvgRect_.setAttribute("y",b);this.markerSvgRect_.setAttribute("width",c);this.markerSvgRect_.setAttribute("height",d);this.currentMarkerSvg=this.markerSvgRect_};Blockly.blockRendering.MarkerSvg.prototype.flipRtl_=function(a){a.setAttribute("transform","scale(-1 1)")}; -Blockly.blockRendering.MarkerSvg.prototype.hide=function(){this.markerSvgLine_.style.display="none";this.markerSvgRect_.style.display="none";this.markerInput_.style.display="none";this.markerBlock_.style.display="none"}; -Blockly.blockRendering.MarkerSvg.prototype.draw=function(a,b){if(b){this.constants_=this.workspace_.getRenderer().getConstants();var c=this.isCursor()?this.constants_.CURSOR_COLOUR:this.constants_.MARKER_COLOUR;this.colour_=this.marker_.colour||c;this.applyColour_();this.showAtLocation_(b);this.firemarkerEvent_(a,b);a=this.currentMarkerSvg.childNodes[0];void 0!==a&&a.beginElement&&a.beginElement()}else this.hide()}; -Blockly.blockRendering.MarkerSvg.prototype.showAtLocation_=function(a){a.getType()==Blockly.ASTNode.types.BLOCK?(a=a.getLocation(),this.showWithBlockPrevOutput_(a)):a.getType()==Blockly.ASTNode.types.OUTPUT?(a=a.getLocation().getSourceBlock(),this.showWithBlockPrevOutput_(a)):a.getLocation().type==Blockly.INPUT_VALUE?this.showWithInput_(a):a.getLocation().type==Blockly.NEXT_STATEMENT?this.showWithNext_(a):a.getType()==Blockly.ASTNode.types.PREVIOUS?(a=a.getLocation().getSourceBlock(),this.showWithBlockPrevOutput_(a)): -a.getType()==Blockly.ASTNode.types.FIELD?this.showWithField_(a):a.getType()==Blockly.ASTNode.types.WORKSPACE?this.showWithCoordinates_(a):a.getType()==Blockly.ASTNode.types.STACK&&this.showWithStack_(a)};Blockly.blockRendering.MarkerSvg.prototype.firemarkerEvent_=function(a,b){var c=b.getSourceBlock(),d=this.isCursor()?"cursorMove":"markerMove";a=new Blockly.Events.Ui(c,d,a,b);b.getType()==Blockly.ASTNode.types.WORKSPACE&&(a.workspaceId=b.getLocation().id);Blockly.Events.fire(a)}; +Blockly.blockRendering.MarkerSvg.prototype.hide=function(){this.markerSvgLine_.style.display="none";this.markerSvgRect_.style.display="none";this.markerInput_.style.display="none";this.markerBlock_.style.display="none"};Blockly.blockRendering.MarkerSvg.prototype.fireMarkerEvent_=function(a,b){var c=b.getSourceBlock(),d=this.isCursor()?"cursorMove":"markerMove";a=new Blockly.Events.Ui(c,d,a,b);b.getType()==Blockly.ASTNode.types.WORKSPACE&&(a.workspaceId=b.getLocation().id);Blockly.Events.fire(a)}; Blockly.blockRendering.MarkerSvg.prototype.getBlinkProperties_=function(){return{attributeType:"XML",attributeName:"fill",dur:"1s",values:this.colour_+";transparent;transparent;",repeatCount:"indefinite"}}; Blockly.blockRendering.MarkerSvg.prototype.createDomInternal_=function(){this.markerSvg_=Blockly.utils.dom.createSvgElement("g",{width:this.constants_.CURSOR_WS_WIDTH,height:this.constants_.WS_CURSOR_HEIGHT},this.svgGroup_);this.markerSvgLine_=Blockly.utils.dom.createSvgElement("rect",{width:this.constants_.CURSOR_WS_WIDTH,height:this.constants_.WS_CURSOR_HEIGHT,style:"display: none"},this.markerSvg_);this.markerSvgRect_=Blockly.utils.dom.createSvgElement("rect",{"class":"blocklyVerticalMarker",rx:10, ry:10,style:"display: none"},this.markerSvg_);this.markerInput_=Blockly.utils.dom.createSvgElement("path",{transform:"",style:"display: none"},this.markerSvg_);this.markerBlock_=Blockly.utils.dom.createSvgElement("path",{transform:"",style:"display: none",fill:"none","stroke-width":this.constants_.CURSOR_STROKE_WIDTH},this.markerSvg_);if(this.isCursor()){var a=this.getBlinkProperties_();Blockly.utils.dom.createSvgElement("animate",a,this.markerSvgLine_);Blockly.utils.dom.createSvgElement("animate", a,this.markerInput_);a.attributeName="stroke";Blockly.utils.dom.createSvgElement("animate",a,this.markerBlock_)}return this.markerSvg_}; -Blockly.blockRendering.MarkerSvg.prototype.applyColour_=function(){this.markerSvgLine_.setAttribute("fill",this.colour_);this.markerSvgRect_.setAttribute("stroke",this.colour_);this.markerInput_.setAttribute("fill",this.colour_);this.markerBlock_.setAttribute("stroke",this.colour_);if(this.isCursor()){var a=this.colour_+";transparent;transparent;";this.markerSvgLine_.firstChild.setAttribute("values",a);this.markerInput_.firstChild.setAttribute("values",a);this.markerBlock_.firstChild.setAttribute("values", -a)}};Blockly.blockRendering.MarkerSvg.prototype.dispose=function(){this.svgGroup_&&Blockly.utils.dom.removeNode(this.svgGroup_)};Blockly.blockRendering.Types={NONE:0,FIELD:1,HAT:2,ICON:4,SPACER:8,BETWEEN_ROW_SPACER:16,IN_ROW_SPACER:32,EXTERNAL_VALUE_INPUT:64,INPUT:128,INLINE_INPUT:256,STATEMENT_INPUT:512,CONNECTION:1024,PREVIOUS_CONNECTION:2048,NEXT_CONNECTION:4096,OUTPUT_CONNECTION:8192,CORNER:16384,LEFT_SQUARE_CORNER:32768,LEFT_ROUND_CORNER:65536,RIGHT_SQUARE_CORNER:131072,RIGHT_ROUND_CORNER:262144,JAGGED_EDGE:524288,ROW:1048576,TOP_ROW:2097152,BOTTOM_ROW:4194304,INPUT_ROW:8388608}; +Blockly.blockRendering.MarkerSvg.prototype.applyColour_=function(a){this.markerSvgLine_.setAttribute("fill",this.colour_);this.markerSvgRect_.setAttribute("stroke",this.colour_);this.markerInput_.setAttribute("fill",this.colour_);this.markerBlock_.setAttribute("stroke",this.colour_);this.isCursor()&&(a=this.colour_+";transparent;transparent;",this.markerSvgLine_.firstChild.setAttribute("values",a),this.markerInput_.firstChild.setAttribute("values",a),this.markerBlock_.firstChild.setAttribute("values", +a))};Blockly.blockRendering.MarkerSvg.prototype.dispose=function(){this.svgGroup_&&Blockly.utils.dom.removeNode(this.svgGroup_)};Blockly.blockRendering.Types={NONE:0,FIELD:1,HAT:2,ICON:4,SPACER:8,BETWEEN_ROW_SPACER:16,IN_ROW_SPACER:32,EXTERNAL_VALUE_INPUT:64,INPUT:128,INLINE_INPUT:256,STATEMENT_INPUT:512,CONNECTION:1024,PREVIOUS_CONNECTION:2048,NEXT_CONNECTION:4096,OUTPUT_CONNECTION:8192,CORNER:16384,LEFT_SQUARE_CORNER:32768,LEFT_ROUND_CORNER:65536,RIGHT_SQUARE_CORNER:131072,RIGHT_ROUND_CORNER:262144,JAGGED_EDGE:524288,ROW:1048576,TOP_ROW:2097152,BOTTOM_ROW:4194304,INPUT_ROW:8388608}; Blockly.blockRendering.Types.LEFT_CORNER=Blockly.blockRendering.Types.LEFT_SQUARE_CORNER|Blockly.blockRendering.Types.LEFT_ROUND_CORNER;Blockly.blockRendering.Types.RIGHT_CORNER=Blockly.blockRendering.Types.RIGHT_SQUARE_CORNER|Blockly.blockRendering.Types.RIGHT_ROUND_CORNER;Blockly.blockRendering.Types.nextTypeValue_=16777216; Blockly.blockRendering.Types.getType=function(a){Blockly.blockRendering.Types.hasOwnProperty(a)||(Blockly.blockRendering.Types[a]=Blockly.blockRendering.Types.nextTypeValue_,Blockly.blockRendering.Types.nextTypeValue_<<=1);return Blockly.blockRendering.Types[a]};Blockly.blockRendering.Types.isField=function(a){return a.type&Blockly.blockRendering.Types.FIELD};Blockly.blockRendering.Types.isHat=function(a){return a.type&Blockly.blockRendering.Types.HAT}; Blockly.blockRendering.Types.isIcon=function(a){return a.type&Blockly.blockRendering.Types.ICON};Blockly.blockRendering.Types.isSpacer=function(a){return a.type&Blockly.blockRendering.Types.SPACER};Blockly.blockRendering.Types.isInRowSpacer=function(a){return a.type&Blockly.blockRendering.Types.IN_ROW_SPACER};Blockly.blockRendering.Types.isInput=function(a){return a.type&Blockly.blockRendering.Types.INPUT};Blockly.blockRendering.Types.isExternalInput=function(a){return a.type&Blockly.blockRendering.Types.EXTERNAL_VALUE_INPUT}; @@ -1231,7 +1248,7 @@ Blockly.blockRendering.Renderer.prototype.makeDebugger_=function(){if(!Blockly.b Blockly.blockRendering.Renderer.prototype.getConstants=function(){return this.constants_};Blockly.blockRendering.Renderer.prototype.shouldHighlightConnection=function(a){return!0};Blockly.blockRendering.Renderer.prototype.orphanCanConnectAtEnd=function(a,b,c){c==Blockly.OUTPUT_VALUE?(c=b.outputConnection,a=Blockly.Connection.lastConnectionInRow(a,b)):(c=b.previousConnection,a=a.lastConnectionInStack());return a?c.checkType(a):!1}; Blockly.blockRendering.Renderer.prototype.getConnectionPreviewMethod=function(a,b,c){return b.type==Blockly.OUTPUT_VALUE||b.type==Blockly.PREVIOUS_STATEMENT?!a.isConnected()||this.orphanCanConnectAtEnd(c,a.targetBlock(),b.type)?Blockly.InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER:Blockly.InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE:Blockly.InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER}; Blockly.blockRendering.Renderer.prototype.render=function(a){Blockly.blockRendering.useDebugger&&!a.renderingDebugger&&(a.renderingDebugger=this.makeDebugger_());var b=this.makeRenderInfo_(a);b.measure();this.makeDrawer_(a,b).draw()};Blockly.geras={};Blockly.geras.ConstantProvider=function(){Blockly.geras.ConstantProvider.superClass_.constructor.call(this);this.FIELD_TEXT_BASELINE_CENTER=!1;this.DARK_PATH_OFFSET=1;this.MAX_BOTTOM_WIDTH=30};Blockly.utils.object.inherits(Blockly.geras.ConstantProvider,Blockly.blockRendering.ConstantProvider); -Blockly.geras.ConstantProvider.prototype.getCSS_=function(a){return Blockly.geras.ConstantProvider.superClass_.getCSS_.call(this,a).concat([a+" .blocklyInsertionMarker>.blocklyPathLight,",a+" .blocklyInsertionMarker>.blocklyPathDark {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none","}"])};Blockly.geras.Highlighter=function(a){this.info_=a;this.inlineSteps_=this.steps_="";this.RTL_=this.info_.RTL;a=a.getRenderer();this.constants_=a.getConstants();this.highlightConstants_=a.getHighlightConstants();this.highlightOffset_=this.highlightConstants_.OFFSET;this.outsideCornerPaths_=this.highlightConstants_.OUTSIDE_CORNER;this.insideCornerPaths_=this.highlightConstants_.INSIDE_CORNER;this.puzzleTabPaths_=this.highlightConstants_.PUZZLE_TAB;this.notchPaths_=this.highlightConstants_.NOTCH;this.startPaths_= +Blockly.geras.ConstantProvider.prototype.getCSS_=function(a){return Blockly.geras.ConstantProvider.superClass_.getCSS_.call(this,a).concat([a+" .blocklyInsertionMarker>.blocklyPathLight,",a+" .blocklyInsertionMarker>.blocklyPathDark {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none;","}"])};Blockly.geras.Highlighter=function(a){this.info_=a;this.inlineSteps_=this.steps_="";this.RTL_=this.info_.RTL;a=a.getRenderer();this.constants_=a.getConstants();this.highlightConstants_=a.getHighlightConstants();this.highlightOffset_=this.highlightConstants_.OFFSET;this.outsideCornerPaths_=this.highlightConstants_.OUTSIDE_CORNER;this.insideCornerPaths_=this.highlightConstants_.INSIDE_CORNER;this.puzzleTabPaths_=this.highlightConstants_.PUZZLE_TAB;this.notchPaths_=this.highlightConstants_.NOTCH;this.startPaths_= this.highlightConstants_.START_HAT;this.jaggedTeethPaths_=this.highlightConstants_.JAGGED_TEETH};Blockly.geras.Highlighter.prototype.getPath=function(){return this.steps_+"\n"+this.inlineSteps_}; Blockly.geras.Highlighter.prototype.drawTopCorner=function(a){this.steps_+=Blockly.utils.svgPaths.moveBy(a.xPos,this.info_.startY);for(var b=0,c;c=a.elements[b];b++)Blockly.blockRendering.Types.isLeftSquareCorner(c)?this.steps_+=this.highlightConstants_.START_POINT:Blockly.blockRendering.Types.isLeftRoundedCorner(c)?this.steps_+=this.outsideCornerPaths_.topLeft(this.RTL_):Blockly.blockRendering.Types.isPreviousConnection(c)?this.steps_+=this.notchPaths_.pathLeft:Blockly.blockRendering.Types.isHat(c)? this.steps_+=this.startPaths_.path(this.RTL_):Blockly.blockRendering.Types.isSpacer(c)&&0!=c.width&&(this.steps_+=Blockly.utils.svgPaths.lineOnAxis("H",c.xPos+c.width-this.highlightOffset_));this.steps_+=Blockly.utils.svgPaths.lineOnAxis("H",a.xPos+a.width-this.highlightOffset_)};Blockly.geras.Highlighter.prototype.drawJaggedEdge_=function(a){this.info_.RTL&&(this.steps_+=this.jaggedTeethPaths_.pathLeft+Blockly.utils.svgPaths.lineOnAxis("v",a.height-this.jaggedTeethPaths_.height-this.highlightOffset_))}; @@ -1326,10 +1343,10 @@ Blockly.zelos.ConstantProvider.prototype.createDom=function(a,b,c){Blockly.zelos {result:"outBlur"},b);Blockly.utils.dom.createSvgElement("feFuncA",{type:"table",tableValues:"0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"},c);Blockly.utils.dom.createSvgElement("feFlood",{"flood-color":this.SELECTED_GLOW_COLOUR,"flood-opacity":1,result:"outColor"},b);Blockly.utils.dom.createSvgElement("feComposite",{"in":"outColor",in2:"outBlur",operator:"in",result:"outGlow"},b);this.selectedGlowFilterId=b.id;this.selectedGlowFilter_=b;a=Blockly.utils.dom.createSvgElement("filter",{id:"blocklyReplacementGlowFilter"+ this.randomIdentifier,height:"160%",width:"180%",y:"-30%",x:"-40%"},a);Blockly.utils.dom.createSvgElement("feGaussianBlur",{"in":"SourceGraphic",stdDeviation:this.REPLACEMENT_GLOW_SIZE},a);b=Blockly.utils.dom.createSvgElement("feComponentTransfer",{result:"outBlur"},a);Blockly.utils.dom.createSvgElement("feFuncA",{type:"table",tableValues:"0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"},b);Blockly.utils.dom.createSvgElement("feFlood",{"flood-color":this.REPLACEMENT_GLOW_COLOUR,"flood-opacity":1,result:"outColor"}, a);Blockly.utils.dom.createSvgElement("feComposite",{"in":"outColor",in2:"outBlur",operator:"in",result:"outGlow"},a);Blockly.utils.dom.createSvgElement("feComposite",{"in":"SourceGraphic",in2:"outGlow",operator:"over"},a);this.replacementGlowFilterId=a.id;this.replacementGlowFilter_=a}; -Blockly.zelos.ConstantProvider.prototype.getCSS_=function(a){return[a+" .blocklyText, ",a+" .blocklyFlyoutLabelText {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-size: "+this.FIELD_TEXT_FONTSIZE+"pt;","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+";","}",a+" .blocklyText {","fill: #fff;","}",a+" .blocklyNonEditableText>rect:not(.blocklyDropdownRect),",a+" .blocklyEditableText>rect:not(.blocklyDropdownRect) {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","}",a+" .blocklyNonEditableText>text,", -a+" .blocklyEditableText>text,",a+" .blocklyNonEditableText>g>text,",a+" .blocklyEditableText>g>text {","fill: #575E75;","}",a+" .blocklyFlyoutLabelText {","fill: #575E75;","}",a+" .blocklyText.blocklyBubbleText {","fill: #575E75;","}",a+" .blocklyDraggable:not(.blocklyDisabled)"," .blocklyEditableText:not(.editing):hover>rect ,",a+" .blocklyDraggable:not(.blocklyDisabled)"," .blocklyEditableText:not(.editing):hover>.blocklyPath {","stroke: #fff;","stroke-width: 2;","}",a+" .blocklyHtmlInput {","font-family: "+ -this.FIELD_TEXT_FONTFAMILY+";","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+";","color: #575E75;","}",a+" .blocklyDropdownText {","fill: #fff !important;","}",a+".blocklyWidgetDiv .goog-menuitem,",a+".blocklyDropDownDiv .goog-menuitem {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","}",a+".blocklyDropDownDiv .goog-menuitem-content {","color: #fff;","}",a+" .blocklyHighlightedConnectionPath {","stroke: "+this.SELECTED_GLOW_COLOUR+";","}",a+" .blocklyDisabled > .blocklyOutlinePath {","fill: url(#blocklyDisabledPattern"+ -this.randomIdentifier+")","}",a+" .blocklyInsertionMarker>.blocklyPath {","fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none","}"]};Blockly.zelos.TopRow=function(a){Blockly.zelos.TopRow.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.zelos.TopRow,Blockly.blockRendering.TopRow);Blockly.zelos.TopRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.zelos.TopRow.prototype.hasLeftSquareCorner=function(a){var b=(a.hat?"cap"===a.hat:this.constants_.ADD_START_HATS)&&!a.outputConnection&&!a.previousConnection;return!!a.outputConnection||b}; +Blockly.zelos.ConstantProvider.prototype.getCSS_=function(a){return[a+" .blocklyText,",a+" .blocklyFlyoutLabelText {","font: "+this.FIELD_TEXT_FONTWEIGHT+" "+this.FIELD_TEXT_FONTSIZE+"pt "+this.FIELD_TEXT_FONTFAMILY+";","}",a+" .blocklyText {","fill: #fff;","}",a+" .blocklyNonEditableText>rect:not(.blocklyDropdownRect),",a+" .blocklyEditableText>rect:not(.blocklyDropdownRect) {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","}",a+" .blocklyNonEditableText>text,",a+" .blocklyEditableText>text,",a+" .blocklyNonEditableText>g>text,", +a+" .blocklyEditableText>g>text {","fill: #575E75;","}",a+" .blocklyFlyoutLabelText {","fill: #575E75;","}",a+" .blocklyText.blocklyBubbleText {","fill: #575E75;","}",a+" .blocklyDraggable:not(.blocklyDisabled)"," .blocklyEditableText:not(.editing):hover>rect,",a+" .blocklyDraggable:not(.blocklyDisabled)"," .blocklyEditableText:not(.editing):hover>.blocklyPath {","stroke: #fff;","stroke-width: 2;","}",a+" .blocklyHtmlInput {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","font-weight: "+this.FIELD_TEXT_FONTWEIGHT+ +";","color: #575E75;","}",a+" .blocklyDropdownText {","fill: #fff !important;","}",a+".blocklyWidgetDiv .goog-menuitem,",a+".blocklyDropDownDiv .goog-menuitem {","font-family: "+this.FIELD_TEXT_FONTFAMILY+";","}",a+".blocklyDropDownDiv .goog-menuitem-content {","color: #fff;","}",a+" .blocklyHighlightedConnectionPath {","stroke: "+this.SELECTED_GLOW_COLOUR+";","}",a+" .blocklyDisabled > .blocklyOutlinePath {","fill: url(#blocklyDisabledPattern"+this.randomIdentifier+")","}",a+" .blocklyInsertionMarker>.blocklyPath {", +"fill-opacity: "+this.INSERTION_MARKER_OPACITY+";","stroke: none;","}"]};Blockly.zelos.TopRow=function(a){Blockly.zelos.TopRow.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.zelos.TopRow,Blockly.blockRendering.TopRow);Blockly.zelos.TopRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.zelos.TopRow.prototype.hasLeftSquareCorner=function(a){var b=(a.hat?"cap"===a.hat:this.constants_.ADD_START_HATS)&&!a.outputConnection&&!a.previousConnection;return!!a.outputConnection||b}; Blockly.zelos.TopRow.prototype.hasRightSquareCorner=function(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection};Blockly.zelos.BottomRow=function(a){Blockly.zelos.BottomRow.superClass_.constructor.call(this,a)};Blockly.utils.object.inherits(Blockly.zelos.BottomRow,Blockly.blockRendering.BottomRow);Blockly.zelos.BottomRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.zelos.BottomRow.prototype.hasLeftSquareCorner=function(a){return!!a.outputConnection}; Blockly.zelos.BottomRow.prototype.hasRightSquareCorner=function(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection};Blockly.zelos.RightConnectionShape=function(a){Blockly.zelos.RightConnectionShape.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.getType("RIGHT_CONNECTION");this.width=this.height=0};Blockly.utils.object.inherits(Blockly.zelos.RightConnectionShape,Blockly.blockRendering.Measurable);Blockly.zelos.StatementInput=function(a,b){Blockly.zelos.StatementInput.superClass_.constructor.call(this,a,b);if(this.connectedBlock){for(a=this.connectedBlock;a.getNextBlock();)a=a.getNextBlock();a.nextConnection||(this.height=this.connectedBlockHeight,this.connectedBottomNextConnection=!0)}};Blockly.utils.object.inherits(Blockly.zelos.StatementInput,Blockly.blockRendering.StatementInput);Blockly.zelos.RenderInfo=function(a,b){Blockly.zelos.RenderInfo.superClass_.constructor.call(this,a,b);this.topRow=new Blockly.zelos.TopRow(this.constants_);this.bottomRow=new Blockly.zelos.BottomRow(this.constants_);this.isInline=!0;this.isMultiRow=!b.getInputsInline()||b.isCollapsed();this.hasStatementInput=0} List of variable names. + * @this {Blockly.Block} + */ + getVars: function() { + return this.arguments_; + }, /** * Return all variables referenced by this block. * @return {!Array.} List of variable models. @@ -836,7 +844,7 @@ Blockly.Blocks['procedures_callnoreturn'] = { var name = this.getProcedureCall(); var def = Blockly.Procedures.getDefinition(name, this.workspace); if (def && (def.type != this.defType_ || - JSON.stringify(def.arguments_) != JSON.stringify(this.arguments_))) { + JSON.stringify(def.getVars()) != JSON.stringify(this.arguments_))) { // The signatures don't match. def = null; } @@ -959,6 +967,7 @@ Blockly.Blocks['procedures_callreturn'] = { updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_, mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom, domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation, + getVars: Blockly.Blocks['procedures_callnoreturn'].getVars, getVarModels: Blockly.Blocks['procedures_callnoreturn'].getVarModels, onchange: Blockly.Blocks['procedures_callnoreturn'].onchange, customContextMenu: diff --git a/blocks_compressed.js b/blocks_compressed.js index 174f4dec9..9f6749596 100644 --- a/blocks_compressed.js +++ b/blocks_compressed.js @@ -1,8 +1,16 @@ // Do not edit this file; automatically generated by gulp. -'use strict'; - -Blockly.Blocks.colour={};Blockly.Constants={};Blockly.Constants.Colour={};Blockly.Constants.Colour.HUE=20; +/* eslint-disable */ +;(function(root, factory) { + if (typeof define === 'function' && define.amd) { // AMD + define(['./blockly_compressed.js'], factory); + } else if (typeof exports === 'object') { // Node.js + module.exports = factory(require('./blockly_compressed.js')); + } else { // Browser + root.Blockly.Blocks = factory(root.Blockly); + } +}(this, function(Blockly) { + 'use strict';Blockly.Blocks.colour={};Blockly.Constants={};Blockly.Constants.Colour={};Blockly.Constants.Colour.HUE=20; Blockly.defineBlocksWithJsonArray([{type:"colour_picker",message0:"%1",args0:[{type:"field_colour",name:"COLOUR",colour:"#ff0000"}],output:"Colour",helpUrl:"%{BKY_COLOUR_PICKER_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_PICKER_TOOLTIP}",extensions:["parent_tooltip_when_inline"]},{type:"colour_random",message0:"%{BKY_COLOUR_RANDOM_TITLE}",output:"Colour",helpUrl:"%{BKY_COLOUR_RANDOM_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RANDOM_TOOLTIP}"},{type:"colour_rgb",message0:"%{BKY_COLOUR_RGB_TITLE} %{BKY_COLOUR_RGB_RED} %1 %{BKY_COLOUR_RGB_GREEN} %2 %{BKY_COLOUR_RGB_BLUE} %3", args0:[{type:"input_value",name:"RED",check:"Number",align:"RIGHT"},{type:"input_value",name:"GREEN",check:"Number",align:"RIGHT"},{type:"input_value",name:"BLUE",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_RGB_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_RGB_TOOLTIP}"},{type:"colour_blend",message0:"%{BKY_COLOUR_BLEND_TITLE} %{BKY_COLOUR_BLEND_COLOUR1} %1 %{BKY_COLOUR_BLEND_COLOUR2} %2 %{BKY_COLOUR_BLEND_RATIO} %3",args0:[{type:"input_value",name:"COLOUR1",check:"Colour", align:"RIGHT"},{type:"input_value",name:"COLOUR2",check:"Colour",align:"RIGHT"},{type:"input_value",name:"RATIO",check:"Number",align:"RIGHT"}],output:"Colour",helpUrl:"%{BKY_COLOUR_BLEND_HELPURL}",style:"colour_blocks",tooltip:"%{BKY_COLOUR_BLEND_TOOLTIP}"}]);Blockly.Blocks.lists={};Blockly.Constants.Lists={};Blockly.Constants.Lists.HUE=260; @@ -113,13 +121,13 @@ this.getProcedureCall())&&(this.setFieldValue(b,"NAME"),this.setTooltip((this.ou this.setCollapsed(!1);this.quarkIds_||(this.quarkConnections_={},this.quarkIds_=[]);c=this.rendered;this.rendered=!1;for(var e=0;e} List of variable names. - * @package */ Blockly.Block.prototype.getVars = function() { var vars = []; @@ -1295,24 +1314,94 @@ Blockly.Block.prototype.setCollapsed = function(collapsed) { Blockly.Block.prototype.toString = function(opt_maxLength, opt_emptyToken) { var text = []; var emptyFieldPlaceholder = opt_emptyToken || '?'; - if (this.collapsed_) { - text.push(this.getInput('_TEMP_COLLAPSED_INPUT').fieldRow[0].getText()); - } else { - for (var i = 0, input; (input = this.inputList[i]); i++) { - for (var j = 0, field; (field = input.fieldRow[j]); j++) { - text.push(field.getText()); - } - if (input.connection) { - var child = input.connection.targetBlock(); - if (child) { - text.push(child.toString(undefined, opt_emptyToken)); - } else { + + // Temporarily set flag to navigate to all fields. + var prevNavigateFields = Blockly.ASTNode.NAVIGATE_ALL_FIELDS; + Blockly.ASTNode.NAVIGATE_ALL_FIELDS = true; + + var node = Blockly.ASTNode.createBlockNode(this); + var rootNode = node; + + /** + * Whether or not to add parentheses around an input. + * @param {!Blockly.Connection} connection The connection. + * @return {boolean} True if we should add parentheses around the input. + */ + function shouldAddParentheses(connection) { + var checks = connection.getCheck(); + if (!checks && connection.targetConnection) { + checks = connection.targetConnection.getCheck(); + } + return !!checks && (checks.indexOf('Boolean') != -1 || + checks.indexOf('Number') != -1); + } + + /** + * Check that we haven't circled back to the original root node. + */ + function checkRoot() { + if (node && node.getType() == rootNode.getType() && + node.getLocation() == rootNode.getLocation()) { + node = null; + } + } + + // Traverse the AST building up our text string. + while (node) { + switch (node.getType()) { + case Blockly.ASTNode.types.INPUT: + var connection = /** @type {!Blockly.Connection} */ (node.getLocation()); + if (!node.in()) { text.push(emptyFieldPlaceholder); + } else if (shouldAddParentheses(connection)) { + text.push('('); } + break; + case Blockly.ASTNode.types.FIELD: + var field = /** @type {Blockly.Field} */ (node.getLocation()); + if (field.name != Blockly.Block.COLLAPSED_FIELD_NAME) { + text.push(field.getText()); + } + break; + } + + var current = node; + node = current.in() || current.next(); + if (!node) { + // Can't go in or next, keep going out until we can go next. + node = current.out(); + checkRoot(); + while (node && !node.next()) { + node = node.out(); + checkRoot(); + // If we hit an input on the way up, possibly close out parentheses. + if (node && node.getType() == Blockly.ASTNode.types.INPUT && + shouldAddParentheses( + /** @type {!Blockly.Connection} */ (node.getLocation()))) { + text.push(')'); + } + } + if (node) { + node = node.next(); } } } - text = text.join(' ').trim() || '???'; + + // Restore state of NAVIGATE_ALL_FIELDS. + Blockly.ASTNode.NAVIGATE_ALL_FIELDS = prevNavigateFields; + + // Run through our text array and simplify expression to remove parentheses + // around single field blocks. + for (var i = 2, l = text.length; i < l; i++) { + if (text[i - 2] == '(' && text[i] == ')') { + text[i - 2] = text[i - 1]; + text.splice(i - 1, 2); + l -= 2; + } + } + + // Join the text array, removing spaces around added paranthesis. + text = text.join(' ').replace(/(\() | (\))/gmi, '$1$2').trim() || '???'; if (opt_maxLength) { // TODO: Improve truncation so that text from this block is given priority. // E.g. "1+2+3+4+5+6+7+8+9=0" should be "...6+7+8+9=0", not "1+2+3+4+5...". @@ -1716,7 +1805,8 @@ Blockly.Block.prototype.moveNumberedInputBefore = function( /** * Remove an input from this block. * @param {string} name The name of the input. - * @param {boolean=} opt_quiet True to prevent error if input is not present. + * @param {boolean=} opt_quiet True to prevent an error if input is not present. + * @return {boolean} True if operation succeeds, false if input is not present and opt_quiet is true * @throws {Error} if the input is not present and opt_quiet is not true. */ Blockly.Block.prototype.removeInput = function(name, opt_quiet) { @@ -1727,10 +1817,12 @@ Blockly.Block.prototype.removeInput = function(name, opt_quiet) { } input.dispose(); this.inputList.splice(i, 1); - return; + return true; } } - if (!opt_quiet) { + if (opt_quiet) { + return false; + } else { throw Error('Input not found: ' + name); } }; diff --git a/core/block_dragger.js b/core/block_dragger.js index 1a8b9ea36..c38f4425b 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -162,6 +162,7 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY, this.draggingBlock_.translate(newLoc.x, newLoc.y); Blockly.blockAnimations.disconnectUiEffect(this.draggingBlock_); + this.draggedConnectionManager_.updateAvailableConnections(); } this.draggingBlock_.setDragging(true); // For future consideration: we may be able to put moveToDragSurface inside @@ -170,7 +171,7 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY, this.draggingBlock_.moveToDragSurface(); var toolbox = this.workspace_.getToolbox(); - if (toolbox) { + if (toolbox && typeof toolbox.addStyle == 'function') { var style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; toolbox.addStyle(style); @@ -220,7 +221,7 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { this.dragBlock(e, currentDragDeltaXY); this.dragIconData_ = []; this.fireDragEndEvent_(); - + Blockly.utils.dom.stopTextWidthCache(); Blockly.blockAnimations.disconnectUiStop(); @@ -246,7 +247,7 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { this.workspace_.setResizesEnabled(true); var toolbox = this.workspace_.getToolbox(); - if (toolbox) { + if (toolbox && typeof toolbox.removeStyle == 'function') { var style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; toolbox.removeStyle(style); diff --git a/core/block_svg.js b/core/block_svg.js index bebf725b3..9cb76f0a6 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -32,6 +32,10 @@ goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Rect'); +goog.requireType('Blockly.IASTNodeLocationSvg'); +goog.requireType('Blockly.IBoundedElement'); +goog.requireType('Blockly.ICopyable'); + /** * Class for a block's SVG representation. @@ -42,15 +46,19 @@ goog.require('Blockly.utils.Rect'); * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise * create a new ID. * @extends {Blockly.Block} + * @implements {Blockly.IASTNodeLocationSvg} + * @implements {Blockly.IBoundedElement} + * @implements {Blockly.ICopyable} * @constructor */ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { // Create core elements for the block. /** - * @type {!SVGElement} + * @type {!SVGGElement} * @private */ - this.svgGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); + this.svgGroup_ = /** @type {!SVGGElement} */ ( + Blockly.utils.dom.createSvgElement('g', {}, null)); this.svgGroup_.translate_ = ''; /** @@ -69,6 +77,14 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { /** @type {boolean} */ this.rendered = false; + /** + * Is this block currently rendering? Used to stop recursive render calls + * from actually triggering a re-render. + * @type {boolean} + * @private + */ + this.renderIsInProgress_ = false; + /** @type {!Blockly.WorkspaceSvg} */ this.workspace = workspace; @@ -114,13 +130,6 @@ Blockly.BlockSvg.prototype.height = 0; */ Blockly.BlockSvg.prototype.width = 0; -/** - * Original location of block being dragged. - * @type {Blockly.utils.Coordinate} - * @private - */ -Blockly.BlockSvg.prototype.dragStartXY_ = null; - /** * Map from IDs for warnings text to PIDs of functions to apply them. * Used to be able to maintain multiple warnings. @@ -602,60 +611,51 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { if (this.collapsed_ == collapsed) { return; } - var renderList = []; - // Show/hide the inputs. - for (var i = 0, input; (input = this.inputList[i]); i++) { - renderList.push.apply(renderList, input.setVisible(!collapsed)); - } - - var COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT'; - if (collapsed) { - var icons = this.getIcons(); - for (var i = 0; i < icons.length; i++) { - icons[i].setVisible(false); - } - var text = this.toString(Blockly.COLLAPSE_CHARS); - this.appendDummyInput(COLLAPSED_INPUT_NAME).appendField(text).init(); - - // Add any warnings on enclosed blocks to this block. - var descendants = this.getDescendants(true); - var nextBlock = this.getNextBlock(); - if (nextBlock) { - var index = descendants.indexOf(nextBlock); - descendants.splice(index, descendants.length - index); - } - for (var i = 1, block; (block = descendants[i]); i++) { - if (block.warning) { - this.setWarningText(Blockly.Msg['COLLAPSED_WARNINGS_WARNING'], - Blockly.BlockSvg.COLLAPSED_WARNING_ID); - break; - } - } - } else { - this.removeInput(COLLAPSED_INPUT_NAME); - // Clear any warnings inherited from enclosed blocks. - if (this.warning) { - this.warning.setText('', Blockly.BlockSvg.COLLAPSED_WARNING_ID); - if (!Object.keys(this.warning.text_).length) { - this.setWarningText(null); - } - } - } Blockly.BlockSvg.superClass_.setCollapsed.call(this, collapsed); + if (!collapsed) { + this.updateCollapsed_(); + } else if (this.rendered) { + this.render(); + // Don't bump neighbours. Users like to store collapsed functions together + // and bumping makes them go out of alignment. + } +}; - if (!renderList.length) { - // No child blocks, just render this block. - renderList[0] = this; - } - if (this.rendered) { - for (var i = 0, block; (block = renderList[i]); i++) { - block.render(); +/** + * Makes sure that when the block is collapsed, it is rendered correctly + * for that state. + * @private + */ +Blockly.BlockSvg.prototype.updateCollapsed_ = function() { + var collapsed = this.isCollapsed(); + var collapsedInputName = Blockly.Block.COLLAPSED_INPUT_NAME; + var collapsedFieldName = Blockly.Block.COLLAPSED_FIELD_NAME; + + for (var i = 0, input; (input = this.inputList[i]); i++) { + if (input.name != collapsedInputName) { + input.setVisible(!collapsed); } - // Don't bump neighbours. - // Although bumping neighbours would make sense, users often collapse - // all their functions and store them next to each other. Expanding and - // bumping causes all their definitions to go out of alignment. } + + if (!collapsed) { + this.removeInput(collapsedInputName); + return; + } + + var icons = this.getIcons(); + for (var i = 0, icon; (icon = icons[i]); i++) { + icon.setVisible(false); + } + + var text = this.toString(Blockly.COLLAPSE_CHARS); + var field = this.getField(collapsedFieldName); + if (field) { + field.setValue(text); + return; + } + var input = this.getInput(collapsedInputName) || + this.appendDummyInput(collapsedInputName); + input.appendField(new Blockly.FieldLabel(text), collapsedFieldName); }; /** @@ -925,7 +925,7 @@ Blockly.BlockSvg.prototype.setInsertionMarker = function(insertionMarker) { /** * Return the root node of the SVG or null if none exists. - * @return {!SVGElement} The root SVG node (probably a group). + * @return {!SVGGElement} The root SVG node (probably a group). */ Blockly.BlockSvg.prototype.getSvgRoot = function() { return this.svgGroup_; @@ -992,6 +992,26 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { Blockly.utils.dom.stopTextWidthCache(); }; +/** + * Encode a block for copying. + * @return {!Blockly.ICopyable.CopyData} Copy metadata. + * @package + */ +Blockly.BlockSvg.prototype.toCopyData = function() { + var xml = Blockly.Xml.blockToDom(this, true); + // Copy only the selected block and internal blocks. + Blockly.Xml.deleteNext(xml); + // Encode start position in XML. + var xy = this.getRelativeToSurfaceXY(); + xml.setAttribute('x', this.RTL ? -xy.x : xy.x); + xml.setAttribute('y', xy.y); + return { + xml: xml, + source: this.workspace, + typeCounts: Blockly.utils.getBlockTypeCounts(this, true) + }; +}; + /** * Change the colour of a block. * @package @@ -1109,22 +1129,22 @@ Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) { text = null; } - // Bubble up to add a warning on top-most collapsed block. - var parent = this.getSurroundParent(); - var collapsedParent = null; - while (parent) { - if (parent.isCollapsed()) { - collapsedParent = parent; - } - parent = parent.getSurroundParent(); - } - if (collapsedParent) { - collapsedParent.setWarningText(Blockly.Msg['COLLAPSED_WARNINGS_WARNING'], - Blockly.BlockSvg.COLLAPSED_WARNING_ID); - } - var changedState = false; if (typeof text == 'string') { + // Bubble up to add a warning on top-most collapsed block. + var parent = this.getSurroundParent(); + var collapsedParent = null; + while (parent) { + if (parent.isCollapsed()) { + collapsedParent = parent; + } + parent = parent.getSurroundParent(); + } + if (collapsedParent) { + collapsedParent.setWarningText(Blockly.Msg['COLLAPSED_WARNINGS_WARNING'], + Blockly.BlockSvg.COLLAPSED_WARNING_ID); + } + if (!this.warning) { this.warning = new Blockly.Warning(this); changedState = true; @@ -1369,17 +1389,20 @@ Blockly.BlockSvg.prototype.setInputsInline = function(newBoolean) { * Remove an input from this block. * @param {string} name The name of the input. * @param {boolean=} opt_quiet True to prevent error if input is not present. + * @return {boolean} True if operation succeeds, false if input is not present and opt_quiet is true * @throws {Error} if the input is not present and * opt_quiet is not true. */ Blockly.BlockSvg.prototype.removeInput = function(name, opt_quiet) { - Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet); + var removed = Blockly.BlockSvg.superClass_.removeInput.call(this, name, opt_quiet); if (this.rendered) { this.render(); // Removing an input will cause the block to change shape. this.bumpNeighbours(); } + + return removed; }; /** @@ -1647,31 +1670,40 @@ Blockly.BlockSvg.prototype.getRootBlock = function() { }; /** - * Render the block. * Lays out and reflows a block based on its contents and settings. * @param {boolean=} opt_bubble If false, just render this block. * If true, also render block's parent, grandparent, etc. Defaults to true. */ Blockly.BlockSvg.prototype.render = function(opt_bubble) { - Blockly.utils.dom.startTextWidthCache(); - this.rendered = true; - (/** @type {!Blockly.WorkspaceSvg} */ (this.workspace)) - .getRenderer().render(this); - // No matter how we rendered, connection locations should now be correct. - this.updateConnectionLocations_(); - if (opt_bubble !== false) { - // Render all blocks above this one (propagate a reflow). - var parentBlock = this.getParent(); - if (parentBlock) { - parentBlock.render(true); - } else { - // Top-most block. Fire an event to allow scrollbars to resize. - this.workspace.resizeContents(); - } + if (this.renderIsInProgress_) { + return; // Don't allow recursive renders. } - Blockly.utils.dom.stopTextWidthCache(); + this.renderIsInProgress_ = true; + try { + this.rendered = true; + Blockly.utils.dom.startTextWidthCache(); - this.updateMarkers_(); + if (this.isCollapsed()) { + this.updateCollapsed_(); + } + this.workspace.getRenderer().render(this); + this.updateConnectionLocations_(); + + if (opt_bubble !== false) { + var parentBlock = this.getParent(); + if (parentBlock) { + parentBlock.render(true); + } else { + // Top-most block. Fire an event to allow scrollbars to resize. + this.workspace.resizeContents(); + } + } + + Blockly.utils.dom.stopTextWidthCache(); + this.updateMarkers_(); + } finally { + this.renderIsInProgress_ = false; + } }; /** diff --git a/core/blockly.js b/core/blockly.js index 209c22e96..c036c2b68 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -26,6 +26,7 @@ goog.require('Blockly.Tooltip'); goog.require('Blockly.Touch'); goog.require('Blockly.utils'); goog.require('Blockly.utils.colour'); +goog.require('Blockly.utils.Size'); goog.require('Blockly.Variables'); goog.require('Blockly.WidgetDiv'); goog.require('Blockly.WorkspaceSvg'); @@ -51,7 +52,7 @@ Blockly.mainWorkspace = null; /** * Currently selected block. - * @type {Blockly.Block} + * @type {?Blockly.ICopyable} */ Blockly.selected = null; @@ -107,13 +108,11 @@ Blockly.EventData; /** * Returns the dimensions of the specified SVG image. * @param {!SVGElement} svg SVG image. - * @return {!Object} Contains width and height properties. + * @return {!Blockly.utils.Size} Contains width and height properties. */ Blockly.svgSize = function(svg) { - return { - width: svg.cachedWidth_, - height: svg.cachedHeight_ - }; + svg = /** @type {?} */ (svg); + return new Blockly.utils.Size(svg.cachedWidth_, svg.cachedHeight_); }; /** @@ -160,7 +159,7 @@ Blockly.svgResize = function(workspace) { /** * Handle a key-down on SVG drawing surface. Does nothing if the main workspace * is not visible. - * @param {!Event} e Key down event. + * @param {!KeyboardEvent} e Key down event. * @package */ // TODO (https://github.com/google/blockly/issues/1998) handle cases where there @@ -190,7 +189,7 @@ Blockly.onKeyDown = function(e) { // Pressing esc closes the context menu. Blockly.hideChaff(); Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_EXIT); - } else if (Blockly.navigation.onKeyPress(e)) { + } else if (!Blockly.Gesture.inProgress() && Blockly.navigation.onKeyPress(e)) { // If the keyboard or field handled the key press return. return; } else if (e.keyCode == Blockly.utils.KeyCodes.BACKSPACE || @@ -249,6 +248,10 @@ Blockly.onKeyDown = function(e) { // 'z' for undo 'Z' is for redo. Blockly.hideChaff(); mainWorkspace.undo(e.shiftKey); + } else if (e.ctrlKey && e.keyCode == Blockly.utils.KeyCodes.Y) { + // Ctrl-y is redo in Windows. Command-y is never valid on Macs. + Blockly.hideChaff(); + mainWorkspace.undo(true); } } // Common code for delete and cut. @@ -264,32 +267,20 @@ Blockly.onKeyDown = function(e) { /** * Copy a block or workspace comment onto the local clipboard. - * @param {!Blockly.Block | !Blockly.WorkspaceComment} toCopy Block or - * Workspace Comment to be copied. + * @param {!Blockly.ICopyable} toCopy Block or Workspace Comment to be copied. * @private */ Blockly.copy_ = function(toCopy) { - if (toCopy.isComment) { - var xml = toCopy.toXmlWithXY(); - } else { - var xml = Blockly.Xml.blockToDom(toCopy, true); - // Copy only the selected block and internal blocks. - Blockly.Xml.deleteNext(xml); - // Encode start position in XML. - var xy = toCopy.getRelativeToSurfaceXY(); - xml.setAttribute('x', toCopy.RTL ? -xy.x : xy.x); - xml.setAttribute('y', xy.y); - } - Blockly.clipboardXml_ = xml; - Blockly.clipboardSource_ = toCopy.workspace; - Blockly.clipboardTypeCounts_ = toCopy.isComment ? null : - Blockly.utils.getBlockTypeCounts(toCopy, true); + var data = toCopy.toCopyData(); + Blockly.clipboardXml_ = data.xml; + Blockly.clipboardSource_ = data.source; + Blockly.clipboardTypeCounts_ = data.typeCounts; }; /** * Duplicate this block and its children, or a workspace comment. - * @param {!Blockly.Block | !Blockly.WorkspaceComment} toDuplicate Block or - * Workspace Comment to be copied. + * @param {!Blockly.ICopyable} toDuplicate Block or Workspace Comment to be + * copied. * @package */ Blockly.duplicate = function(toDuplicate) { diff --git a/core/bubble.js b/core/bubble.js index 7916541bf..7208d8492 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -21,6 +21,8 @@ goog.require('Blockly.utils.math'); goog.require('Blockly.utils.userAgent'); goog.require('Blockly.Workspace'); +goog.requireType('Blockly.utils.Metrics'); + /** * Class for UI bubble. @@ -489,8 +491,8 @@ Blockly.Bubble.prototype.layoutBubble_ = function() { * workspace (what percentage of the bubble is visible). * @param {!{x: number, y: number}} relativeMin The position of the top-left * corner of the bubble relative to the anchor point. - * @param {!Object} metrics The metrics of the workspace the bubble will - * appear in. + * @param {!Blockly.utils.Metrics} metrics The metrics of the workspace the + * bubble will appear in. * @return {number} The percentage of the bubble that is visible. * @private */ @@ -535,10 +537,10 @@ Blockly.Bubble.prototype.getOverlap_ = function(relativeMin, metrics) { * Calculate what the optimal horizontal position of the top-left corner of the * bubble is (relative to the anchor point) so that the most area of the * bubble is shown. - * @param {!Object} metrics The metrics of the workspace the bubble will - * appear in. + * @param {!Blockly.utils.Metrics} metrics The metrics of the workspace the + * bubble will appear in. * @return {number} The optimal horizontal position of the top-left corner - * of the bubble. + * of the bubble. * @private */ Blockly.Bubble.prototype.getOptimalRelativeLeft_ = function(metrics) { @@ -593,10 +595,10 @@ Blockly.Bubble.prototype.getOptimalRelativeLeft_ = function(metrics) { * Calculate what the optimal vertical position of the top-left corner of * the bubble is (relative to the anchor point) so that the most area of the * bubble is shown. - * @param {!Object} metrics The metrics of the workspace the bubble will - * appear in. + * @param {!Blockly.utils.Metrics} metrics The metrics of the workspace the + * bubble will appear in. * @return {number} The optimal vertical position of the top-left corner - * of the bubble. + * of the bubble. * @private */ Blockly.Bubble.prototype.getOptimalRelativeTop_ = function(metrics) { diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js index 4976163af..66c16459e 100644 --- a/core/bubble_dragger.js +++ b/core/bubble_dragger.js @@ -107,7 +107,7 @@ Blockly.BubbleDragger.prototype.startBubbleDrag = function() { this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true); var toolbox = this.workspace_.getToolbox(); - if (toolbox) { + if (toolbox && typeof toolbox.addStyle == 'function') { var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; toolbox.addStyle(style); @@ -208,10 +208,11 @@ Blockly.BubbleDragger.prototype.endBubbleDrag = function( } this.workspace_.setResizesEnabled(true); - if (this.workspace_.getToolbox()) { + var toolbox = this.workspace_.getToolbox(); + if (toolbox && typeof toolbox.removeStyle == 'function') { var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; - this.workspace_.getToolbox().removeStyle(style); + toolbox.removeStyle(style); } Blockly.Events.setGroup(false); }; diff --git a/core/comment.js b/core/comment.js index e3b076f49..54c102b08 100644 --- a/core/comment.js +++ b/core/comment.js @@ -86,7 +86,7 @@ Blockly.utils.object.inherits(Blockly.Comment, Blockly.Icon); /** * Draw the comment icon. * @param {!Element} group The icon group. - * @private + * @protected */ Blockly.Comment.prototype.drawIcon_ = function(group) { // Circle. diff --git a/core/components/component.js b/core/components/component.js index 2a19d5734..cca8285d5 100644 --- a/core/components/component.js +++ b/core/components/component.js @@ -84,6 +84,13 @@ Blockly.Component = function() { * @private */ this.childIndex_ = {}; + + /** + * Whether or not the component has been disposed. + * @type {boolean} + * @private + */ + this.disposed_ = false; }; @@ -115,7 +122,12 @@ Blockly.Component.Error = { * Error when an attempt is made to add a child component at an out-of-bounds * index. We don't support sparse child arrays. */ - CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds' + CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds', + + /** + * Error when calling an abstract method that should be overriden. + */ + ABSTRACT_METHOD: 'Unimplemented abstract method' }; /** @@ -195,12 +207,11 @@ Blockly.Component.prototype.isInDocument = function() { }; /** - * Creates the initial DOM representation for the component. The default - * implementation is to set this.element_ = div. + * Creates the initial DOM representation for the component. * @protected */ Blockly.Component.prototype.createDom = function() { - this.element_ = document.createElement('div'); + throw Error(Blockly.Component.Error.ABSTRACT_METHOD); }; /** @@ -223,19 +234,6 @@ Blockly.Component.prototype.render = function(opt_parentElement) { this.render_(opt_parentElement); }; -/** - * Renders the component before another element. The other element should be in - * the document already. - * - * Throws an Error if the component is already rendered. - * - * @param {Node} sibling Node to render the component before. - * @protected - */ -Blockly.Component.prototype.renderBefore = function(sibling) { - this.render_(/** @type {Element} */ (sibling.parentNode), sibling); -}; - /** * Renders the component. If a parent element is supplied, the component's * element will be appended to it. If there is no optional parent element and @@ -476,7 +474,8 @@ Blockly.Component.prototype.addChildAt = function(child, index, opt_render) { child.element_.parentNode && // Under some circumstances, IE8 implicitly creates a Document Fragment // for detached nodes, so ensure the parent is an Element as it should be. - child.element_.parentNode.nodeType == Blockly.utils.dom.Node.ELEMENT_NODE) { + child.element_.parentNode.nodeType == + Blockly.utils.dom.NodeType.ELEMENT_NODE) { // We don't touch the DOM, but if the parent is in the document, and the // child element is in the document but not marked as such, then we call // enterDocument on the child. @@ -496,21 +495,6 @@ Blockly.Component.prototype.getContentElement = function() { return this.element_; }; -/** - * Set is right-to-left. This function should be used if the component needs - * to know the rendering direction during DOM creation (i.e. before - * {@link #enterDocument} is called and is right-to-left is set). - * @param {boolean} rightToLeft Whether the component is rendered - * right-to-left. - * @package - */ -Blockly.Component.prototype.setRightToLeft = function(rightToLeft) { - if (this.inDocument_) { - throw Error(Blockly.Component.Error.ALREADY_RENDERED); - } - this.rightToLeft_ = rightToLeft; -}; - /** * Returns true if the component has children. * @return {boolean} True if the component has children. @@ -568,14 +552,3 @@ Blockly.Component.prototype.forEachChild = function(f, opt_obj) { f.call(/** @type {?} */ (opt_obj), this.children_[i], i); } }; - -/** - * Returns the 0-based index of the given child component, or -1 if no such - * child is found. - * @param {?Blockly.Component} child The child component. - * @return {number} 0-based index of the child component; -1 if not found. - * @protected - */ -Blockly.Component.prototype.indexOfChild = function(child) { - return this.children_.indexOf(child); -}; diff --git a/core/components/menu/menu.js b/core/components/menu/menu.js deleted file mode 100644 index 79ed646b6..000000000 --- a/core/components/menu/menu.js +++ /dev/null @@ -1,543 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly menu similar to Closure's goog.ui.Menu - * @author samelh@google.com (Sam El-Husseini) - */ -'use strict'; - -goog.provide('Blockly.Menu'); - -goog.require('Blockly.Component'); -goog.require('Blockly.utils.aria'); -goog.require('Blockly.utils.Coordinate'); -goog.require('Blockly.utils.dom'); -goog.require('Blockly.utils.object'); - - -/** - * A basic menu class. - * @constructor - * @extends {Blockly.Component} - */ -Blockly.Menu = function() { - Blockly.Component.call(this); - - /** - * Coordinates of the mousedown event that caused this menu to open. Used to - * prevent the consequent mouseup event due to a simple click from activating - * a menu item immediately. - * @type {?Blockly.utils.Coordinate} - * @package - */ - this.openingCoords = null; - - /** - * This is the element that we will listen to the real focus events on. - * A value of -1 means no menuitem is highlighted. - * @type {number} - * @private - */ - this.highlightedIndex_ = -1; - - /** - * Mouse over event data. - * @type {?Blockly.EventData} - * @private - */ - this.mouseOverHandler_ = null; - - /** - * Click event data. - * @type {?Blockly.EventData} - * @private - */ - this.clickHandler_ = null; - - /** - * Mouse enter event data. - * @type {?Blockly.EventData} - * @private - */ - this.mouseEnterHandler_ = null; - - /** - * Mouse leave event data. - * @type {?Blockly.EventData} - * @private - */ - this.mouseLeaveHandler_ = null; - - /** - * Key down event data. - * @type {?Blockly.EventData} - * @private - */ - this.onKeyDownWrapper_ = null; -}; -Blockly.utils.object.inherits(Blockly.Menu, Blockly.Component); - - -/** - * Creates the menu DOM. - * @override - */ -Blockly.Menu.prototype.createDom = function() { - var element = document.createElement('div'); - element.id = this.getId(); - this.setElementInternal(element); - - // Set class - element.className = 'goog-menu goog-menu-vertical blocklyNonSelectable'; - element.tabIndex = 0; - - // Initialize ARIA role. - Blockly.utils.aria.setRole(element, - this.roleName_ || Blockly.utils.aria.Role.MENU); -}; - -/** - * Focus the menu element. - * @package - */ -Blockly.Menu.prototype.focus = function() { - var el = this.getElement(); - if (el) { - el.focus({preventScroll:true}); - Blockly.utils.dom.addClass(el, 'focused'); - } -}; - -/** - * Blur the menu element. - * @package - */ -Blockly.Menu.prototype.blur = function() { - var el = this.getElement(); - if (el) { - el.blur(); - Blockly.utils.dom.removeClass(el, 'focused'); - } -}; - -/** - * Set the menu accessibility role. - * @param {!Blockly.utils.aria.Role} roleName role name. - * @package - */ -Blockly.Menu.prototype.setRole = function(roleName) { - this.roleName_ = roleName; -}; - -/** @override */ -Blockly.Menu.prototype.enterDocument = function() { - Blockly.Menu.superClass_.enterDocument.call(this); - - this.forEachChild(function(child) { - if (child.isInDocument()) { - this.registerChildId_(child); - } - }, this); - - this.attachEvents_(); -}; - -/** - * Cleans up the container before its DOM is removed from the document, and - * removes event handlers. Overrides {@link Blockly.Component#exitDocument}. - * @override - */ -Blockly.Menu.prototype.exitDocument = function() { - // {@link #setHighlightedIndex} has to be called before - // {@link Blockly.Component#exitDocument}, otherwise it has no effect. - this.setHighlightedIndex(-1); - - Blockly.Menu.superClass_.exitDocument.call(this); -}; - -/** @override */ -Blockly.Menu.prototype.disposeInternal = function() { - Blockly.Menu.superClass_.disposeInternal.call(this); - - this.detachEvents_(); -}; - -/** - * Adds the event listeners to the menu. - * @private - */ -Blockly.Menu.prototype.attachEvents_ = function() { - var el = /** @type {!EventTarget} */ (this.getElement()); - - this.mouseOverHandler_ = Blockly.bindEventWithChecks_(el, - 'mouseover', this, this.handleMouseOver_, true); - this.clickHandler_ = Blockly.bindEventWithChecks_(el, - 'click', this, this.handleClick_, true); - this.mouseEnterHandler_ = Blockly.bindEventWithChecks_(el, - 'mouseenter', this, this.handleMouseEnter_, true); - this.mouseLeaveHandler_ = Blockly.bindEventWithChecks_(el, - 'mouseleave', this, this.handleMouseLeave_, true); - this.onKeyDownWrapper_ = Blockly.bindEventWithChecks_(el, - 'keydown', this, this.handleKeyEvent); -}; - -/** - * Removes the event listeners from the menu. - * @private - */ -Blockly.Menu.prototype.detachEvents_ = function() { - if (this.mouseOverHandler_) { - Blockly.unbindEvent_(this.mouseOverHandler_); - this.mouseOverHandler_ = null; - } - if (this.clickHandler_) { - Blockly.unbindEvent_(this.clickHandler_); - this.clickHandler_ = null; - } - if (this.mouseEnterHandler_) { - Blockly.unbindEvent_(this.mouseEnterHandler_); - this.mouseEnterHandler_ = null; - } - if (this.mouseLeaveHandler_) { - Blockly.unbindEvent_(this.mouseLeaveHandler_); - this.mouseLeaveHandler_ = null; - } - if (this.onKeyDownWrapper_) { - Blockly.unbindEvent_(this.onKeyDownWrapper_); - this.onKeyDownWrapper_ = null; - } -}; - -// Child component management. - -/** - * Map of DOM IDs to child menuitems. Each key is the DOM ID of a child - * menuitems's root element; each value is a reference to the child menu - * item itself. - * @type {?Object} - * @private - */ -Blockly.Menu.prototype.childElementIdMap_ = null; - -/** - * Creates a DOM ID for the child menuitem and registers it to an internal - * hash table to be able to find it fast by id. - * @param {Blockly.Component} child The child menuitem. Its root element has - * to be created yet. - * @private - */ -Blockly.Menu.prototype.registerChildId_ = function(child) { - // Map the DOM ID of the menuitem's root element to the menuitem itself. - var childElem = child.getElement(); - - // If the menuitem's root element doesn't have a DOM ID assign one. - var id = childElem.id || (childElem.id = child.getId()); - - // Lazily create the child element ID map on first use. - if (!this.childElementIdMap_) { - this.childElementIdMap_ = {}; - } - this.childElementIdMap_[id] = child; -}; - -/** - * Returns the child menuitem that owns the given DOM node, or null if no such - * menuitem is found. - * @param {Node} node DOM node whose owner is to be returned. - * @return {?Blockly.MenuItem} menuitem for which the DOM node belongs to. - * @protected - */ -Blockly.Menu.prototype.getMenuItem = function(node) { - // Ensure that this menu actually has child menuitems before - // looking up the menuitem. - if (this.childElementIdMap_) { - var elem = this.getElement(); - while (node && node !== elem) { - var id = node.id; - if (id in this.childElementIdMap_) { - return this.childElementIdMap_[id]; - } - node = node.parentNode; - } - } - return null; -}; - -// Highlight management. - -/** - * Unhighlight the current highlighted item. - * @protected - */ -Blockly.Menu.prototype.unhighlightCurrent = function() { - var highlighted = this.getHighlighted(); - if (highlighted) { - highlighted.setHighlighted(false); - } -}; - -/** - * Clears the currently highlighted item. - * @protected - */ -Blockly.Menu.prototype.clearHighlighted = function() { - this.unhighlightCurrent(); - this.setHighlightedIndex(-1); -}; - -/** - * Returns the currently highlighted item (if any). - * @return {?Blockly.Component} Highlighted item (null if none). - * @protected - */ -Blockly.Menu.prototype.getHighlighted = function() { - return this.getChildAt(this.highlightedIndex_); -}; - -/** - * Highlights the item at the given 0-based index (if any). If another item - * was previously highlighted, it is un-highlighted. - * @param {number} index Index of item to highlight (-1 removes the current - * highlight). - * @protected - */ -Blockly.Menu.prototype.setHighlightedIndex = function(index) { - var child = this.getChildAt(index); - if (child) { - child.setHighlighted(true); - this.highlightedIndex_ = index; - } else if (this.highlightedIndex_ > -1) { - this.getHighlighted().setHighlighted(false); - this.highlightedIndex_ = -1; - } - - // Bring the highlighted item into view. This has no effect if the menu is not - // scrollable. - if (child) { - Blockly.utils.style.scrollIntoContainerView( - /** @type {!Element} */ (child.getElement()), - /** @type {!Element} */ (this.getElement())); - } -}; - -/** - * Highlights the given item if it exists and is a child of the container; - * otherwise un-highlights the currently highlighted item. - * @param {Blockly.MenuItem} item Item to highlight. - * @protected - */ -Blockly.Menu.prototype.setHighlighted = function(item) { - this.setHighlightedIndex(this.indexOfChild(item)); -}; - -/** - * Highlights the next highlightable item (or the first if nothing is currently - * highlighted). - * @package - */ -Blockly.Menu.prototype.highlightNext = function() { - this.unhighlightCurrent(); - this.highlightHelper(function(index, max) { - return (index + 1) % max; - }, this.highlightedIndex_); -}; - -/** - * Highlights the previous highlightable item (or the last if nothing is - * currently highlighted). - * @package - */ -Blockly.Menu.prototype.highlightPrevious = function() { - this.unhighlightCurrent(); - this.highlightHelper(function(index, max) { - index--; - return index < 0 ? max - 1 : index; - }, this.highlightedIndex_); -}; - -/** - * Helper function that manages the details of moving the highlight among - * child menuitems in response to keyboard events. - * @param {function(this: Blockly.Component, number, number) : number} fn - * Function that accepts the current and maximum indices, and returns the - * next index to check. - * @param {number} startIndex Start index. - * @return {boolean} Whether the highlight has changed. - * @protected - */ -Blockly.Menu.prototype.highlightHelper = function(fn, startIndex) { - // If the start index is -1 (meaning there's nothing currently highlighted), - // try starting from the currently open item, if any. - var curIndex = - startIndex < 0 ? -1 : startIndex; - var numItems = this.getChildCount(); - - curIndex = fn.call(this, curIndex, numItems); - var visited = 0; - while (visited <= numItems) { - var menuItem = /** @type {Blockly.MenuItem} */ (this.getChildAt(curIndex)); - if (menuItem && this.canHighlightItem(menuItem)) { - this.setHighlightedIndex(curIndex); - return true; - } - visited++; - curIndex = fn.call(this, curIndex, numItems); - } - return false; -}; - -/** - * Returns whether the given item can be highlighted. - * @param {Blockly.MenuItem} item The item to check. - * @return {boolean} Whether the item can be highlighted. - * @protected - */ -Blockly.Menu.prototype.canHighlightItem = function(item) { - return item.isEnabled(); -}; - -// Mouse events. - -/** - * Handles mouseover events. Highlight menuitems as the user - * hovers over them. - * @param {Event} e Mouse event to handle. - * @private - */ -Blockly.Menu.prototype.handleMouseOver_ = function(e) { - var menuItem = this.getMenuItem(/** @type {Node} */ (e.target)); - - if (menuItem) { - if (menuItem.isEnabled()) { - var currentHighlighted = this.getHighlighted(); - if (currentHighlighted === menuItem) { - return; - } - - this.unhighlightCurrent(); - this.setHighlighted(menuItem); - } else { - this.unhighlightCurrent(); - } - } -}; - -/** - * Handles click events. Pass the event onto the child - * menuitem to handle. - * @param {Event} e Click to handle. - * @private - */ -Blockly.Menu.prototype.handleClick_ = function(e) { - var oldCoords = this.openingCoords; - // Clear out the saved opening coords immediately so they're not used twice. - this.openingCoords = null; - if (oldCoords && typeof e.clientX === 'number') { - var newCoords = new Blockly.utils.Coordinate(e.clientX, e.clientY); - if (Blockly.utils.Coordinate.distance(oldCoords, newCoords) < 1) { - // This menu was opened by a mousedown and we're handling the consequent - // click event. The coords haven't changed, meaning this was the same - // opening event. Don't do the usual behavior because the menu just popped - // up under the mouse and the user didn't mean to activate this item. - return; - } - } - - var menuItem = this.getMenuItem(/** @type {Node} */ (e.target)); - - if (menuItem && menuItem.handleClick(e)) { - e.preventDefault(); - } -}; - -/** - * Handles mouse enter events. Focus the element. - * @param {Event} _e Mouse event to handle. - * @private - */ -Blockly.Menu.prototype.handleMouseEnter_ = function(_e) { - this.focus(); -}; - -/** - * Handles mouse leave events. Blur and clear highlight. - * @param {Event} _e Mouse event to handle. - * @private - */ -Blockly.Menu.prototype.handleMouseLeave_ = function(_e) { - if (this.getElement()) { - this.blur(); - this.clearHighlighted(); - } -}; - -// Keyboard events. - -/** - * Attempts to handle a keyboard event, if the menuitem is enabled, by calling - * {@link handleKeyEventInternal}. Considered protected; should only be used - * within this package and by subclasses. - * @param {Event} e Key event to handle. - * @return {boolean} Whether the key event was handled. - * @protected - */ -Blockly.Menu.prototype.handleKeyEvent = function(e) { - if (this.getChildCount() != 0 && - this.handleKeyEventInternal(e)) { - e.preventDefault(); - e.stopPropagation(); - return true; - } - return false; -}; - -/** - * Attempts to handle a keyboard event; returns true if the event was handled, - * false otherwise. If the container is enabled, and a child is highlighted, - * calls the child menuitem's `handleKeyEvent` method to give the menuitem - * a chance to handle the event first. - * @param {Event} e Key event to handle. - * @return {boolean} Whether the event was handled by the container (or one of - * its children). - * @protected - */ -Blockly.Menu.prototype.handleKeyEventInternal = function(e) { - // Give the highlighted menuitem the chance to handle the key event. - var highlighted = this.getHighlighted(); - if (highlighted && typeof highlighted.handleKeyEvent == 'function' && - highlighted.handleKeyEvent(e)) { - return true; - } - - // Do not handle the key event if any modifier key is pressed. - if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) { - return false; - } - - // Either nothing is highlighted, or the highlighted menuitem didn't handle - // the key event, so attempt to handle it here. - switch (e.keyCode) { - case Blockly.utils.KeyCodes.ENTER: - if (highlighted) { - highlighted.performActionInternal(e); - } - break; - - case Blockly.utils.KeyCodes.UP: - this.highlightPrevious(); - break; - - case Blockly.utils.KeyCodes.DOWN: - this.highlightNext(); - break; - - default: - return false; - } - - return true; -}; diff --git a/core/components/menu/menuitem.js b/core/components/menu/menuitem.js deleted file mode 100644 index ae163f30b..000000000 --- a/core/components/menu/menuitem.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Blockly menu item similar to Closure's goog.ui.MenuItem - * @author samelh@google.com (Sam El-Husseini) - */ -'use strict'; - -goog.provide('Blockly.MenuItem'); - -goog.require('Blockly.Component'); -goog.require('Blockly.utils.aria'); -goog.require('Blockly.utils.dom'); -goog.require('Blockly.utils.object'); - - -/** - * Class representing an item in a menu. - * - * @param {string} content Text caption to display as the content of - * the item. - * @param {string=} opt_value Data/model associated with the menu item. - * @constructor - * @extends {Blockly.Component} - */ -Blockly.MenuItem = function(content, opt_value) { - Blockly.Component.call(this); - - this.setContentInternal(content); - this.setValue(opt_value); - - /** - * @type {boolean} - * @private - */ - this.enabled_ = true; - - /** - * @type {Blockly.MenuItem} - * @private - */ - this.previousSibling_; - - /** - * @type {Blockly.MenuItem} - * @private - */ - this.nextSibling_; -}; -Blockly.utils.object.inherits(Blockly.MenuItem, Blockly.Component); - - -/** - * Creates the menuitem's DOM. - * @override - */ -Blockly.MenuItem.prototype.createDom = function() { - var element = document.createElement('div'); - element.id = this.getId(); - this.setElementInternal(element); - - // Set class and style - element.className = 'goog-menuitem goog-option ' + - (!this.enabled_ ? 'goog-menuitem-disabled ' : '') + - (this.checked_ ? 'goog-option-selected ' : '') + - (this.rightToLeft_ ? 'goog-menuitem-rtl ' : ''); - - var content = this.getContentWrapperDom(); - element.appendChild(content); - - // Add a checkbox for checkable menu items. - var checkboxDom = this.getCheckboxDom(); - if (checkboxDom) { - content.appendChild(checkboxDom); - } - - content.appendChild(this.getContentDom()); - - // Initialize ARIA role and state. - Blockly.utils.aria.setRole(element, this.roleName_ || (this.checkable_ ? - Blockly.utils.aria.Role.MENUITEMCHECKBOX : - Blockly.utils.aria.Role.MENUITEM)); - Blockly.utils.aria.setState(element, Blockly.utils.aria.State.SELECTED, - (this.checkable_ && this.checked_) || false); -}; - -/** - * @return {Element} The HTML element for the checkbox. - * @protected - */ -Blockly.MenuItem.prototype.getCheckboxDom = function() { - if (!this.checkable_) { - return null; - } - var menuItemCheckbox = document.createElement('div'); - menuItemCheckbox.className = 'goog-menuitem-checkbox'; - return menuItemCheckbox; -}; - -/** - * @return {!Element} The HTML for the content. - * @protected - */ -Blockly.MenuItem.prototype.getContentDom = function() { - var content = this.content_; - if (typeof content === 'string') { - content = document.createTextNode(content); - } - return content; -}; - -/** - * @return {!Element} The HTML for the content wrapper. - * @protected - */ -Blockly.MenuItem.prototype.getContentWrapperDom = function() { - var contentWrapper = document.createElement('div'); - contentWrapper.className = 'goog-menuitem-content'; - return contentWrapper; -}; - -/** - * Sets the content associated with the menu item. - * @param {string} content Text caption to set as the - * menuitem's contents. - * @protected - */ -Blockly.MenuItem.prototype.setContentInternal = function(content) { - this.content_ = content; -}; - -/** - * Sets the value associated with the menu item. - * @param {*} value Value to be associated with the menu item. - * @package - */ -Blockly.MenuItem.prototype.setValue = function(value) { - this.value_ = value; -}; - -/** - * Gets the value associated with the menu item. - * @return {*} value Value associated with the menu item. - * @package - */ -Blockly.MenuItem.prototype.getValue = function() { - return this.value_; -}; - -/** - * Set the menu accessibility role. - * @param {!Blockly.utils.aria.Role} roleName Role name. - * @package - */ -Blockly.MenuItem.prototype.setRole = function(roleName) { - this.roleName_ = roleName; -}; - -/** - * Sets the menu item to be checkable or not. Set to true for menu items - * that represent checkable options. - * @param {boolean} checkable Whether the menu item is checkable. - * @package - */ -Blockly.MenuItem.prototype.setCheckable = function(checkable) { - this.checkable_ = checkable; -}; - -/** - * Checks or unchecks the component. - * @param {boolean} checked Whether to check or uncheck the component. - * @package - */ -Blockly.MenuItem.prototype.setChecked = function(checked) { - if (!this.checkable_) { - return; - } - this.checked_ = checked; - - var el = this.getElement(); - if (el && this.isEnabled()) { - if (checked) { - Blockly.utils.dom.addClass(el, 'goog-option-selected'); - Blockly.utils.aria.setState(el, - Blockly.utils.aria.State.SELECTED, true); - } else { - Blockly.utils.dom.removeClass(el, 'goog-option-selected'); - Blockly.utils.aria.setState(el, - Blockly.utils.aria.State.SELECTED, false); - } - } -}; - -/** - * Highlights or unhighlights the component. - * @param {boolean} highlight Whether to highlight or unhighlight the component. - * @package - */ -Blockly.MenuItem.prototype.setHighlighted = function(highlight) { - this.highlight_ = highlight; - - var el = this.getElement(); - if (el && this.isEnabled()) { - if (highlight) { - Blockly.utils.dom.addClass(el, 'goog-menuitem-highlight'); - } else { - Blockly.utils.dom.removeClass(el, 'goog-menuitem-highlight'); - } - } -}; - -/** - * Returns true if the menu item is enabled, false otherwise. - * @return {boolean} Whether the menu item is enabled. - * @package - */ -Blockly.MenuItem.prototype.isEnabled = function() { - return this.enabled_; -}; - -/** - * Enables or disables the menu item. - * @param {boolean} enabled Whether to enable or disable the menu item. - * @package - */ -Blockly.MenuItem.prototype.setEnabled = function(enabled) { - this.enabled_ = enabled; - - var el = this.getElement(); - if (el) { - if (!this.enabled_) { - Blockly.utils.dom.addClass(el, 'goog-menuitem-disabled'); - } else { - Blockly.utils.dom.removeClass(el, 'goog-menuitem-disabled'); - } - } -}; - -/** - * Handles click events. If the component is enabled, trigger - * the action associated with this menu item. - * @param {Event} _e Mouse event to handle. - * @package - */ -Blockly.MenuItem.prototype.handleClick = function(_e) { - if (this.isEnabled()) { - this.setHighlighted(true); - this.performActionInternal(); - } -}; - -/** - * Performs the appropriate action when the menu item is activated - * by the user. - * @protected - */ -Blockly.MenuItem.prototype.performActionInternal = function() { - if (this.checkable_) { - this.setChecked(!this.checked_); - } - if (this.actionHandler_) { - this.actionHandler_.call(/** @type {?} */ (this.actionHandlerObj_), this); - } -}; - -/** - * Set the handler that's triggered when the menu item is activated - * by the user. If `opt_obj` is provided, it will be used as the - * 'this' object in the function when called. - * @param {function(this:T,!Blockly.MenuItem):?} fn The handler. - * @param {T=} opt_obj Used as the 'this' object in f when called. - * @template T - * @package - */ -Blockly.MenuItem.prototype.onAction = function(fn, opt_obj) { - this.actionHandler_ = fn; - this.actionHandlerObj_ = opt_obj; -}; diff --git a/core/components/tree/basenode.js b/core/components/tree/basenode.js index 1a9c8ca1c..bd8485912 100644 --- a/core/components/tree/basenode.js +++ b/core/components/tree/basenode.js @@ -91,13 +91,6 @@ Blockly.tree.BaseNode = function(content, config) { */ this.expanded_ = false; - /** - * Whether to allow user to collapse this node. - * @type {boolean} - * @protected - */ - this.isUserCollapsible_ = true; - /** * Nesting depth of this node; cached result of getDepth. * -1 if value has not been cached. @@ -174,8 +167,7 @@ Blockly.tree.BaseNode.prototype.initAccessibility = function() { var ce = this.getChildrenElement(); if (ce) { - Blockly.utils.aria.setRole(ce, - Blockly.utils.aria.Role.GROUP); + Blockly.utils.aria.setRole(ce, Blockly.utils.aria.Role.GROUP); // In case the children will be created lazily. if (ce.hasChildNodes()) { @@ -201,7 +193,7 @@ Blockly.tree.BaseNode.prototype.initAccessibility = function() { Blockly.tree.BaseNode.prototype.createDom = function() { var element = document.createElement('div'); element.appendChild(this.toDom()); - this.setElementInternal(/** @type {!Element} */ (element)); + this.setElementInternal(/** @type {!HTMLElement} */ (element)); }; @@ -340,25 +332,24 @@ Blockly.tree.BaseNode.prototype.setDepth_ = function(depth) { }; /** - * Returns true if the node is a descendant of this node - * @param {Blockly.tree.BaseNode} node The node to check. + * Returns true if the node is a descendant of this node. + * @param {Blockly.Component} node The node to check. * @return {boolean} True if the node is a descendant of this node, false * otherwise. * @protected */ Blockly.tree.BaseNode.prototype.contains = function(node) { - var current = node; - while (current) { - if (current == this) { + while (node) { + if (node == this) { return true; } - current = current.getParent(); + node = node.getParent(); } return false; }; /** - * This is re-defined here to indicate to the closure compiler the correct + * This is re-defined here to indicate to the Closure Compiler the correct * child return type. * @param {number} index 0-based index. * @return {Blockly.tree.BaseNode} The child at the given index; null if none. @@ -377,6 +368,16 @@ Blockly.tree.BaseNode.prototype.getChildren = function() { return children; }; +/** + * Returns the node's parent, if any. + * @return {?Blockly.tree.BaseNode} The parent node. + * @protected + */ +Blockly.tree.BaseNode.prototype.getParent = function() { + return /** @type {Blockly.tree.BaseNode} */ ( + Blockly.tree.BaseNode.superClass_.getParent.call(this)); +}; + /** * @return {Blockly.tree.BaseNode} The previous sibling of this node. * @protected @@ -551,6 +552,7 @@ Blockly.tree.BaseNode.prototype.toDom = function() { }; /** + * Calculates correct padding for each row. Nested categories are indented more. * @return {number} The pixel indent of the row. * @private */ @@ -559,6 +561,7 @@ Blockly.tree.BaseNode.prototype.getPixelIndent_ = function() { }; /** + * Creates row with icon and label dom. * @return {!Element} The HTML element for the row. * @protected */ @@ -575,6 +578,8 @@ Blockly.tree.BaseNode.prototype.getRowDom = function() { }; /** + * Adds the selected class name to the default row class name if node is + * selected. * @return {string} The class name for the row. * @protected */ @@ -613,10 +618,11 @@ Blockly.tree.BaseNode.prototype.getIconDom = function() { * @protected */ Blockly.tree.BaseNode.prototype.getCalculatedIconClass = function() { - throw Error('unimplemented abstract method'); + throw Error(Blockly.Component.Error.ABSTRACT_METHOD); }; /** + * Gets a string containing the x and y position of the node's background. * @return {string} The background position style value. * @protected */ @@ -626,7 +632,7 @@ Blockly.tree.BaseNode.prototype.getBackgroundPosition = function() { }; /** - * @return {Element} The element for the tree node. + * @return {HTMLElement} The element for the tree node. * @override */ Blockly.tree.BaseNode.prototype.getElement = function() { @@ -635,7 +641,7 @@ Blockly.tree.BaseNode.prototype.getElement = function() { el = document.getElementById(this.getId()); this.setElementInternal(el); } - return el; + return /** @type {!HTMLElement} */ (el); }; /** @@ -707,26 +713,6 @@ Blockly.tree.BaseNode.prototype.updateIcon_ = function() { this.getIconElement().className = this.getCalculatedIconClass(); }; -/** - * Handles mouse down event. - * @param {!Event} e The browser event. - * @protected - */ -Blockly.tree.BaseNode.prototype.onMouseDown = function(e) { - var el = e.target; - // expand icon - var type = el.getAttribute('type'); - if (type == 'expand' && this.hasChildren()) { - if (this.isUserCollapsible_) { - this.toggle(); - } - return; - } - - this.select(); - this.updateRow(); -}; - /** * Handles a click event. * @param {!Event} e The browser event. @@ -746,16 +732,10 @@ Blockly.tree.BaseNode.prototype.onKeyDown = function(e) { var handled = true; switch (e.keyCode) { case Blockly.utils.KeyCodes.RIGHT: - if (e.altKey) { - break; - } handled = this.selectChild(); break; case Blockly.utils.KeyCodes.LEFT: - if (e.altKey) { - break; - } handled = this.selectParent(); break; @@ -767,6 +747,12 @@ Blockly.tree.BaseNode.prototype.onKeyDown = function(e) { handled = this.selectPrevious(); break; + case Blockly.utils.KeyCodes.ENTER: + case Blockly.utils.KeyCodes.SPACE: + this.toggle(); + handled = true; + break; + default: handled = false; } @@ -811,7 +797,7 @@ Blockly.tree.BaseNode.prototype.selectPrevious = function() { * @package */ Blockly.tree.BaseNode.prototype.selectParent = function() { - if (this.hasChildren() && this.expanded_ && this.isUserCollapsible_) { + if (this.hasChildren() && this.expanded_) { this.setExpanded(false); } else { var parent = this.getParent(); @@ -862,18 +848,17 @@ Blockly.tree.BaseNode.prototype.getLastShownDescendant = function() { Blockly.tree.BaseNode.prototype.getNextShownNode = function() { if (this.hasChildren() && this.expanded_) { return this.getChildAt(0); - } else { - var parent = this; - var next; - while (parent != this.getTree()) { - next = parent.getNextSibling(); - if (next != null) { - return next; - } - parent = parent.getParent(); - } - return null; } + var parent = this; + var next; + while (parent != this.getTree()) { + next = parent.getNextSibling(); + if (next != null) { + return next; + } + parent = parent.getParent(); + } + return null; }; /** diff --git a/core/components/tree/treecontrol.js b/core/components/tree/treecontrol.js index 3ec32654a..97ea3e39d 100644 --- a/core/components/tree/treecontrol.js +++ b/core/components/tree/treecontrol.js @@ -33,20 +33,6 @@ goog.require('Blockly.utils.style'); Blockly.tree.TreeControl = function(toolbox, config) { this.toolbox_ = toolbox; - /** - * Focus event data. - * @type {?Blockly.EventData} - * @private - */ - this.onFocusWrapper_ = null; - - /** - * Blur event data. - * @type {?Blockly.EventData} - * @private - */ - this.onBlurWrapper_ = null; - /** * Click event data. * @type {?Blockly.EventData} @@ -66,13 +52,27 @@ Blockly.tree.TreeControl = function(toolbox, config) { // The root is open and selected by default. this.expanded_ = true; this.selected_ = true; - + /** * Currently selected item. * @type {Blockly.tree.BaseNode} * @private */ this.selectedItem_ = this; + + /** + * A handler that's triggered before a node is selected. + * @type {?function(Blockly.tree.BaseNode):boolean} + * @private + */ + this.onBeforeSelected_ = null; + + /** + * A handler that's triggered before a node is selected. + * @type {?function(Blockly.tree.BaseNode, Blockly.tree.BaseNode):?} + * @private + */ + this.onAfterSelected_ = null; }; Blockly.utils.object.inherits(Blockly.tree.TreeControl, Blockly.tree.BaseNode); @@ -101,41 +101,6 @@ Blockly.tree.TreeControl.prototype.getDepth = function() { return 0; }; -/** - * Handles focus on the tree. - * @param {!Event} _e The browser event. - * @private - */ -Blockly.tree.TreeControl.prototype.handleFocus_ = function(_e) { - this.focused_ = true; - var el = /** @type {!Element} */ (this.getElement()); - Blockly.utils.dom.addClass(el, 'focused'); - - if (this.selectedItem_) { - this.selectedItem_.select(); - } -}; - -/** - * Handles blur on the tree. - * @param {!Event} _e The browser event. - * @private - */ -Blockly.tree.TreeControl.prototype.handleBlur_ = function(_e) { - this.focused_ = false; - var el = /** @type {!Element} */ (this.getElement()); - Blockly.utils.dom.removeClass(el, 'focused'); -}; - -/** - * Get whether this tree has focus or not. - * @return {boolean} True if it has focus. - * @package - */ -Blockly.tree.TreeControl.prototype.hasFocus = function() { - return this.focused_; -}; - /** @override */ Blockly.tree.TreeControl.prototype.setExpanded = function(expanded) { this.expanded_ = expanded; @@ -172,11 +137,6 @@ Blockly.tree.TreeControl.prototype.getCalculatedIconClass = function() { if (!expanded && iconClass) { return iconClass; } - - // fall back on default icons - if (expanded && this.config_.cssExpandedRootIcon) { - return this.config_.cssTreeIcon + ' ' + this.config_.cssExpandedRootIcon; - } return ''; }; @@ -278,10 +238,6 @@ Blockly.tree.TreeControl.prototype.attachEvents_ = function() { var el = this.getElement(); el.tabIndex = 0; - this.onFocusWrapper_ = Blockly.bindEvent_(el, - 'focus', this, this.handleFocus_); - this.onBlurWrapper_ = Blockly.bindEvent_(el, - 'blur', this, this.handleBlur_); this.onClickWrapper_ = Blockly.bindEventWithChecks_(el, 'click', this, this.handleMouseEvent_); this.onKeydownWrapper_ = Blockly.bindEvent_(el, @@ -293,14 +249,6 @@ Blockly.tree.TreeControl.prototype.attachEvents_ = function() { * @private */ Blockly.tree.TreeControl.prototype.detachEvents_ = function() { - if (this.onFocusWrapper_) { - Blockly.unbindEvent_(this.onFocusWrapper_); - this.onFocusWrapper_ = null; - } - if (this.onBlurWrapper_) { - Blockly.unbindEvent_(this.onBlurWrapper_); - this.onBlurWrapper_ = null; - } if (this.onClickWrapper_) { Blockly.unbindEvent_(this.onClickWrapper_); this.onClickWrapper_ = null; @@ -318,15 +266,8 @@ Blockly.tree.TreeControl.prototype.detachEvents_ = function() { */ Blockly.tree.TreeControl.prototype.handleMouseEvent_ = function(e) { var node = this.getNodeFromEvent_(e); - if (node) { - switch (e.type) { - case 'mousedown': - node.onMouseDown(e); - break; - case 'click': - node.onClick_(e); - break; - } + if (node && e.type == 'click') { + node.onClick_(e); } }; @@ -337,10 +278,8 @@ Blockly.tree.TreeControl.prototype.handleMouseEvent_ = function(e) { * @private */ Blockly.tree.TreeControl.prototype.handleKeyEvent_ = function(e) { - var handled = false; - // Handle navigation keystrokes. - handled = (this.selectedItem_ && this.selectedItem_.onKeyDown(e)) || handled; + var handled = !!(this.selectedItem_ && this.selectedItem_.onKeyDown(e)); if (handled) { Blockly.utils.style.scrollIntoContainerView( @@ -363,7 +302,7 @@ Blockly.tree.TreeControl.prototype.getNodeFromEvent_ = function(e) { // find the right node var node = null; var target = e.target; - while (target != null) { + while (target) { var id = target.id; node = Blockly.tree.BaseNode.allNodes[id]; if (node) { diff --git a/core/components/tree/treenode.js b/core/components/tree/treenode.js index 612dcb4f4..c6aa3f2d2 100644 --- a/core/components/tree/treenode.js +++ b/core/components/tree/treenode.js @@ -32,6 +32,13 @@ goog.require('Blockly.utils.KeyCodes'); Blockly.tree.TreeNode = function(toolbox, content, config) { this.toolbox_ = toolbox; Blockly.tree.BaseNode.call(this, content, config); + + /** + * A handler that's triggered when the size of node has changed. + * @type {?function():?} + * @private + */ + this.onSizeChanged_ = null; }; Blockly.utils.object.inherits(Blockly.tree.TreeNode, Blockly.tree.BaseNode); @@ -93,7 +100,7 @@ Blockly.tree.TreeNode.prototype.getCalculatedIconClass = function() { */ Blockly.tree.TreeNode.prototype.onClick_ = function(_e) { // Expand icon. - if (this.hasChildren() && this.isUserCollapsible_) { + if (this.hasChildren()) { this.toggle(); this.select(); } else if (this.isSelected()) { @@ -104,15 +111,6 @@ Blockly.tree.TreeNode.prototype.onClick_ = function(_e) { this.updateRow(); }; -/** - * Suppress the inherited mouse down behaviour. - * @param {!Event} _e The browser event. - * @override - * @private - */ -Blockly.tree.TreeNode.prototype.onMouseDown = function(_e) { - // NOP -}; /** * Remap event.keyCode in horizontalLayout so that arrow diff --git a/core/connection.js b/core/connection.js index 88d183157..1fe595ad7 100644 --- a/core/connection.js +++ b/core/connection.js @@ -16,12 +16,15 @@ goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockMove'); goog.require('Blockly.Xml'); +goog.requireType('Blockly.IASTNodeLocationWithBlock'); + /** * Class for a connection between blocks. * @param {!Blockly.Block} source The block establishing this connection. * @param {number} type The type of the connection. * @constructor + * @implements {Blockly.IASTNodeLocationWithBlock} */ Blockly.Connection = function(source, type) { /** @@ -242,7 +245,6 @@ Blockly.Connection.prototype.isConnected = function() { * @param {Blockly.Connection} target Connection to check compatibility with. * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, * an error code otherwise. - * @package */ Blockly.Connection.prototype.canConnectWithReason = function(target) { if (!target) { @@ -617,6 +619,7 @@ Blockly.Connection.prototype.checkType = function(otherConnection) { * @return {boolean} True if the connections share a type. * @private * @deprecated October 2019, use connection.checkType instead. + * @suppress {unusedPrivateMembers} */ Blockly.Connection.prototype.checkType_ = function(otherConnection) { console.warn('Deprecated call to Blockly.Connection.prototype.checkType_, ' + @@ -639,7 +642,7 @@ Blockly.Connection.prototype.onCheckChanged_ = function() { /** * Change a connection's compatibility. - * @param {?(string|!Array)} check Compatible value type or list of + * @param {?(string|!Array.)} check Compatible value type or list of * value types. Null if all types are compatible. * @return {!Blockly.Connection} The connection being modified * (to allow chaining). diff --git a/core/contextmenu.js b/core/contextmenu.js index 1391967b2..f4b10dbfc 100644 --- a/core/contextmenu.js +++ b/core/contextmenu.js @@ -24,7 +24,7 @@ goog.require('Blockly.Msg'); goog.require('Blockly.utils'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); -goog.require('Blockly.utils.uiMenu'); +goog.require('Blockly.utils.Rect'); goog.require('Blockly.utils.userAgent'); goog.require('Blockly.Xml'); @@ -36,11 +36,11 @@ goog.require('Blockly.Xml'); Blockly.ContextMenu.currentBlock = null; /** - * Opaque data that can be passed to unbindEvent_. - * @type {Array.} + * Menu object. + * @type {Blockly.Menu} * @private */ -Blockly.ContextMenu.eventWrapper_ = null; +Blockly.ContextMenu.menu_ = null; /** * Construct the menu based on the list of options and show the menu. @@ -49,17 +49,18 @@ Blockly.ContextMenu.eventWrapper_ = null; * @param {boolean} rtl True if RTL, false if LTR. */ Blockly.ContextMenu.show = function(e, options, rtl) { - Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, null); + Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, Blockly.ContextMenu.dispose); if (!options.length) { Blockly.ContextMenu.hide(); return; } var menu = Blockly.ContextMenu.populate_(options, rtl); + Blockly.ContextMenu.menu_ = menu; Blockly.ContextMenu.position_(menu, e, rtl); // 1ms delay is required for focusing on context menus because some other // mouse event is still waiting in the queue and clears focus. - setTimeout(function() {menu.getElement().focus();}, 1); + setTimeout(function() {menu.focus();}, 1); Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block. }; @@ -77,14 +78,15 @@ Blockly.ContextMenu.populate_ = function(options, rtl) { callback: Blockly.MakeItSo} */ var menu = new Blockly.Menu(); - menu.setRightToLeft(rtl); + menu.setRole(Blockly.utils.aria.Role.MENU); for (var i = 0, option; (option = options[i]); i++) { var menuItem = new Blockly.MenuItem(option.text); menuItem.setRightToLeft(rtl); - menu.addChild(menuItem, true); + menuItem.setRole(Blockly.utils.aria.Role.MENUITEM); + menu.addChild(menuItem); menuItem.setEnabled(option.enabled); if (option.enabled) { - var actionHandler = function() { + var actionHandler = function(_menuItem) { var option = this; Blockly.ContextMenu.hide(); option.callback(); @@ -108,25 +110,28 @@ Blockly.ContextMenu.position_ = function(menu, e, rtl) { var viewportBBox = Blockly.utils.getViewportBBox(); // This one is just a point, but we'll pretend that it's a rect so we can use // some helper functions. - var anchorBBox = { - top: e.clientY + viewportBBox.top, - bottom: e.clientY + viewportBBox.top, - left: e.clientX + viewportBBox.left, - right: e.clientX + viewportBBox.left - }; + var anchorBBox = new Blockly.utils.Rect( + e.clientY + viewportBBox.top, + e.clientY + viewportBBox.top, + e.clientX + viewportBBox.left, + e.clientX + viewportBBox.left + ); Blockly.ContextMenu.createWidget_(menu); - var menuSize = Blockly.utils.uiMenu.getSize(menu); + var menuSize = menu.getSize(); if (rtl) { - Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize); + anchorBBox.left += menuSize.width; + anchorBBox.right += menuSize.width; + viewportBBox.left += menuSize.width; + viewportBBox.right += menuSize.width; } Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, rtl); // Calling menuDom.focus() has to wait until after the menu has been placed // correctly. Otherwise it will cause a page scroll to get the misplaced menu // in view. See issue #1329. - menu.getElement().focus(); + menu.focus(); }; /** @@ -141,8 +146,8 @@ Blockly.ContextMenu.createWidget_ = function(menu) { Blockly.utils.dom.addClass( /** @type {!Element} */ (menuDom), 'blocklyContextMenu'); // Prevent system context menu when right-clicking a Blockly context menu. - Blockly.bindEventWithChecks_( - /** @type {!EventTarget} */ (menuDom), 'contextmenu', null, Blockly.utils.noEvent); + Blockly.bindEventWithChecks_(/** @type {!EventTarget} */ (menuDom), + 'contextmenu', null, Blockly.utils.noEvent); // Focus only after the initial render to avoid issue #1329. menu.focus(); }; @@ -153,9 +158,15 @@ Blockly.ContextMenu.createWidget_ = function(menu) { Blockly.ContextMenu.hide = function() { Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu); Blockly.ContextMenu.currentBlock = null; - if (Blockly.ContextMenu.eventWrapper_) { - Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_); - Blockly.ContextMenu.eventWrapper_ = null; +}; + +/** + * Dispose of the menu. + */ +Blockly.ContextMenu.dispose = function() { + if (Blockly.ContextMenu.menu_) { + Blockly.ContextMenu.menu_.dispose(); + Blockly.ContextMenu.menu_ = null; } }; diff --git a/core/css.js b/core/css.js index 8a29f4b6a..d03769f38 100644 --- a/core/css.js +++ b/core/css.js @@ -74,13 +74,13 @@ Blockly.Css.inject = function(hasCss, pathToMedia) { /** * Set the cursor to be displayed when over something draggable. - * See See https://github.com/google/blockly/issues/981 for context. + * See https://github.com/google/blockly/issues/981 for context. * @param {*} _cursor Enum. * @deprecated April 2017. */ Blockly.Css.setCursor = function(_cursor) { console.warn('Deprecated call to Blockly.Css.setCursor. ' + - 'See https://github.com/google/blockly/issues/981 for context'); + 'See issue #981 for context'); }; /** @@ -150,8 +150,7 @@ Blockly.Css.CONTENT = [ 'box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);', 'color: #000;', 'display: none;', - 'font-family: sans-serif;', - 'font-size: 9pt;', + 'font: 9pt sans-serif;', 'opacity: .9;', 'padding: 2px;', 'position: absolute;', @@ -169,11 +168,11 @@ Blockly.Css.CONTENT = [ 'background-color: #fff;', 'border-radius: 2px;', 'padding: 4px;', - 'box-shadow: 0px 0px 3px 1px rgba(0,0,0,.3);', + 'box-shadow: 0 0 3px 1px rgba(0,0,0,.3);', '}', - '.blocklyDropDownDiv.focused {', - 'box-shadow: 0px 0px 6px 1px rgba(0,0,0,.3);', + '.blocklyDropDownDiv.blocklyFocused {', + 'box-shadow: 0 0 6px 1px rgba(0,0,0,.3);', '}', '.blocklyDropDownContent {', @@ -309,7 +308,7 @@ Blockly.Css.CONTENT = [ '.blocklyInsertionMarker>.blocklyPathLight,', '.blocklyInsertionMarker>.blocklyPathDark {', 'fill-opacity: .2;', - 'stroke: none', + 'stroke: none;', '}', '.blocklyMultilineText {', @@ -333,7 +332,8 @@ Blockly.Css.CONTENT = [ Don't allow users to select text. It gets annoying when trying to drag a block and selected text moves instead. */ - '.blocklySvg text, .blocklyBlockDragSurface text {', + '.blocklySvg text,', + '.blocklyBlockDragSurface text {', 'user-select: none;', '-ms-user-select: none;', '-webkit-user-select: none;', @@ -416,7 +416,8 @@ Blockly.Css.CONTENT = [ 'z-index: 30;', '}', - '.blocklyScrollbarHorizontal, .blocklyScrollbarVertical {', + '.blocklyScrollbarHorizontal,', + '.blocklyScrollbarVertical {', 'position: absolute;', 'outline: none;', '}', @@ -449,217 +450,10 @@ Blockly.Css.CONTENT = [ 'background: #faa;', '}', - '.blocklyContextMenu {', - 'border-radius: 4px;', - 'max-height: 100%;', - '}', - - '.blocklyDropdownMenu {', - 'border-radius: 2px;', - 'padding: 0 !important;', - '}', - - '.blocklyWidgetDiv .blocklyDropdownMenu .goog-menuitem,', - '.blocklyDropDownDiv .blocklyDropdownMenu .goog-menuitem {', - /* 28px on the left for icon or checkbox. */ - 'padding-left: 28px;', - '}', - - /* BiDi override for the resting state. */ - /* #noflip */ - '.blocklyWidgetDiv .blocklyDropdownMenu .goog-menuitem.goog-menuitem-rtl,', - '.blocklyDropDownDiv .blocklyDropdownMenu .goog-menuitem.goog-menuitem-rtl {', - /* Flip left/right padding for BiDi. */ - 'padding-left: 5px;', - 'padding-right: 28px;', - '}', - '.blocklyVerticalMarker {', 'stroke-width: 3px;', 'fill: rgba(255,255,255,.5);', - 'pointer-events: none', - '}', - - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon,', - '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {', - 'background: url(<<>>/sprites.png) no-repeat -48px -16px;', - '}', - - /* Copied from: goog/css/menu.css */ - /* - * Copyright 2009 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /** - * Standard styling for menus created by goog.ui.MenuRenderer. - * - * @author attila@google.com (Attila Bodis) - */ - - '.blocklyWidgetDiv .goog-menu {', - 'background: #fff;', - 'border-color: transparent;', - 'border-style: solid;', - 'border-width: 1px;', - 'cursor: default;', - 'font: normal 13px Arial, sans-serif;', - 'margin: 0;', - 'outline: none;', - 'padding: 4px 0;', - 'position: absolute;', - 'overflow-y: auto;', - 'overflow-x: hidden;', - 'max-height: 100%;', - 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ - 'box-shadow: 0px 0px 3px 1px rgba(0,0,0,.3);', - '}', - - '.blocklyWidgetDiv .goog-menu.focused {', - 'box-shadow: 0px 0px 6px 1px rgba(0,0,0,.3);', - '}', - - '.blocklyDropDownDiv .goog-menu {', - 'cursor: default;', - 'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;', - 'outline: none;', - 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ - '}', - - /* Copied from: goog/css/menuitem.css */ - /* - * Copyright 2009 The Closure Library Authors. All Rights Reserved. - * - * Use of this source code is governed by the Apache License, Version 2.0. - * See the COPYING file for details. - */ - - /** - * Standard styling for menus created by goog.ui.MenuItemRenderer. - * - * @author attila@google.com (Attila Bodis) - */ - - /** - * State: resting. - * - * NOTE(mleibman,chrishenry): - * The RTL support in Closure is provided via two mechanisms -- "rtl" CSS - * classes and BiDi flipping done by the CSS compiler. Closure supports RTL - * with or without the use of the CSS compiler. In order for them not to - * conflict with each other, the "rtl" CSS classes need to have the #noflip - * annotation. The non-rtl counterparts should ideally have them as well, - * but, since .goog-menuitem existed without .goog-menuitem-rtl for so long - * before being added, there is a risk of people having templates where they - * are not rendering the .goog-menuitem-rtl class when in RTL and instead - * rely solely on the BiDi flipping by the CSS compiler. That's why we're - * not adding the #noflip to .goog-menuitem. - */ - '.blocklyWidgetDiv .goog-menuitem,', - '.blocklyDropDownDiv .goog-menuitem {', - 'color: #000;', - 'font: normal 13px Arial, sans-serif;', - 'list-style: none;', - 'margin: 0;', - /* 7em on the right for shortcut. */ - 'min-width: 7em;', - 'border: none;', - 'padding: 6px 15px;', - 'white-space: nowrap;', - 'cursor: pointer;', - '}', - - /* If a menu doesn't have checkable items or items with icons, - * remove padding. - */ - '.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,', - '.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem,', - '.blocklyDropDownDiv .goog-menu-nocheckbox .goog-menuitem,', - '.blocklyDropDownDiv .goog-menu-noicon .goog-menuitem {', - 'padding-left: 12px;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-content,', - '.blocklyDropDownDiv .goog-menuitem-content {', - 'font-family: Arial, sans-serif;', - 'font-size: 13px;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-content {', - 'color: #000;', - '}', - - '.blocklyDropDownDiv .goog-menuitem-content {', - 'color: #000;', - '}', - - /* State: disabled. */ - '.blocklyWidgetDiv .goog-menuitem-disabled,', - '.blocklyDropDownDiv .goog-menuitem-disabled {', - 'cursor: inherit;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content,', - '.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-content {', - 'color: #ccc !important;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon,', - '.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-icon {', - 'opacity: .3;', - 'filter: alpha(opacity=30);', - '}', - - /* State: hover. */ - '.blocklyWidgetDiv .goog-menuitem-highlight ,', - '.blocklyDropDownDiv .goog-menuitem-highlight {', - 'background-color: rgba(0,0,0,.1);', - '}', - - /* State: selected/checked. */ - '.blocklyWidgetDiv .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-icon,', - '.blocklyDropDownDiv .goog-menuitem-checkbox,', - '.blocklyDropDownDiv .goog-menuitem-icon {', - 'background-repeat: no-repeat;', - 'height: 16px;', - 'left: 6px;', - 'position: absolute;', - 'right: auto;', - 'vertical-align: middle;', - 'width: 16px;', - '}', - - /* BiDi override for the selected/checked state. */ - /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon,', - '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', - '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {', - /* Flip left/right positioning. */ - 'left: auto;', - 'right: 6px;', - '}', - - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon,', - '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {', - 'position: static;', /* Scroll with the menu. */ - 'float: left;', - 'margin-left: -24px;', - '}', - - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon,', - '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', - '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {', - 'float: right;', - 'margin-right: -24px;', + 'pointer-events: none;', '}', '.blocklyComputeCanvas {', @@ -671,5 +465,95 @@ Blockly.Css.CONTENT = [ '.blocklyNoPointerEvents {', 'pointer-events: none;', '}', + + '.blocklyContextMenu {', + 'border-radius: 4px;', + 'max-height: 100%;', + '}', + + '.blocklyDropdownMenu {', + 'border-radius: 2px;', + 'padding: 0 !important;', + '}', + + '.blocklyDropdownMenu .blocklyMenuItem {', + /* 28px on the left for icon or checkbox. */ + 'padding-left: 28px;', + '}', + + /* BiDi override for the resting state. */ + '.blocklyDropdownMenu .blocklyMenuItemRtl {', + /* Flip left/right padding for BiDi. */ + 'padding-left: 5px;', + 'padding-right: 28px;', + '}', + + '.blocklyWidgetDiv .blocklyMenu {', + 'background: #fff;', + 'border: 1px solid transparent;', + 'box-shadow: 0 0 3px 1px rgba(0,0,0,.3);', + 'font: normal 13px Arial, sans-serif;', + 'margin: 0;', + 'outline: none;', + 'padding: 4px 0;', + 'position: absolute;', + 'overflow-y: auto;', + 'overflow-x: hidden;', + 'max-height: 100%;', + 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ + '}', + + '.blocklyWidgetDiv .blocklyMenu.blocklyFocused {', + 'box-shadow: 0 0 6px 1px rgba(0,0,0,.3);', + '}', + + '.blocklyDropDownDiv .blocklyMenu {', + 'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;', + 'outline: none;', + 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ + '}', + + /* State: resting. */ + '.blocklyMenuItem {', + 'border: none;', + 'color: #000;', + 'cursor: pointer;', + 'list-style: none;', + 'margin: 0;', + /* 7em on the right for shortcut. */ + 'min-width: 7em;', + 'padding: 6px 15px;', + 'white-space: nowrap;', + '}', + + /* State: disabled. */ + '.blocklyMenuItemDisabled {', + 'color: #ccc;', + 'cursor: inherit;', + '}', + + /* State: hover. */ + '.blocklyMenuItemHighlight {', + 'background-color: rgba(0,0,0,.1);', + '}', + + /* State: selected/checked. */ + '.blocklyMenuItemCheckbox {', + 'height: 16px;', + 'position: absolute;', + 'width: 16px;', + '}', + + '.blocklyMenuItemSelected .blocklyMenuItemCheckbox {', + 'background: url(<<>>/sprites.png) no-repeat -48px -16px;', + 'float: left;', + 'margin-left: -24px;', + 'position: static;', /* Scroll with the menu. */ + '}', + + '.blocklyMenuItemRtl .blocklyMenuItemCheckbox {', + 'float: right;', + 'margin-right: -24px;', + '}', /* eslint-enable indent */ ]; diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index 4c8c3fe2a..770af679f 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -19,6 +19,9 @@ goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.math'); goog.require('Blockly.utils.style'); +goog.requireType('Blockly.utils.Rect'); +goog.requireType('Blockly.utils.Size'); + /** * Class for drop-down div. @@ -117,6 +120,35 @@ Blockly.DropDownDiv.rendererClassName_ = ''; */ Blockly.DropDownDiv.themeClassName_ = ''; +/** + * Dropdown bounds info object used to encapsulate sizing information about a + * bounding element (bounding box and width/height). + * @typedef {{ + * top:number, + * left:number, + * bottom:number, + * right:number, + * width:number, + * height:number + * }} + */ +Blockly.DropDownDiv.BoundsInfo; + +/** + * Dropdown position metrics. + * @typedef {{ + * initialX:number, + * initialY:number, + * finalX:number, + * finalY:number, + * arrowX:?number, + * arrowY:?number, + * arrowAtTop:?boolean, + * arrowVisible:boolean + * }} + */ +Blockly.DropDownDiv.PositionMetrics; + /** * Create and insert the DOM element for this div. * @package @@ -166,10 +198,10 @@ Blockly.DropDownDiv.createDom = function() { // Handle focusin/out events to add a visual indicator when // a child is focused or blurred. div.addEventListener('focusin', function() { - Blockly.utils.dom.addClass(div, 'focused'); + Blockly.utils.dom.addClass(div, 'blocklyFocused'); }); div.addEventListener('focusout', function() { - Blockly.utils.dom.removeClass(div, 'focused'); + Blockly.utils.dom.removeClass(div, 'blocklyFocused'); }); }; @@ -184,7 +216,7 @@ Blockly.DropDownDiv.setBoundsElement = function(boundsElement) { /** * Provide the div for inserting content into the drop-down. - * @return {Element} Div to populate with content + * @return {!Element} Div to populate with content. */ Blockly.DropDownDiv.getContentDiv = function() { return Blockly.DropDownDiv.content_; @@ -214,7 +246,7 @@ Blockly.DropDownDiv.setColour = function(backgroundColour, borderColour) { * and the secondary position above the block. Drop-down will be * constrained to the block's workspace. * @param {!Blockly.Field} field The field showing the drop-down. - * @param {!Blockly.Block} block Block to position the drop-down around. + * @param {!Blockly.BlockSvg} block Block to position the drop-down around. * @param {Function=} opt_onHide Optional callback for when the drop-down is * hidden. * @param {number=} opt_secondaryYOffset Optional Y offset for above-block @@ -250,7 +282,7 @@ Blockly.DropDownDiv.showPositionedByField = function(field, /** * Get the scaled bounding box of a block. - * @param {!Blockly.Block} block The block. + * @param {!Blockly.BlockSvg} block The block. * @return {!Blockly.utils.Rect} The scaled bounding box of the block. * @private */ @@ -302,10 +334,15 @@ Blockly.DropDownDiv.showPositionedByRect_ = function(bBox, field, if (opt_secondaryYOffset) { secondaryY += opt_secondaryYOffset; } - var sourceBlock = field.getSourceBlock(); - // Set bounds to workspace; show the drop-down. + var sourceBlock = /** @type {!Blockly.BlockSvg} */ (field.getSourceBlock()); + // Set bounds to main workspace; show the drop-down. + var workspace = sourceBlock.workspace; + while (workspace.options.parentWorkspace) { + workspace = /** @type {!Blockly.WorkspaceSvg} */ ( + workspace.options.parentWorkspace); + } Blockly.DropDownDiv.setBoundsElement( - sourceBlock.workspace.getParentSvg().parentNode); + /** @type {Element} */ (workspace.getParentSvg().parentNode)); return Blockly.DropDownDiv.show( field, sourceBlock.RTL, primaryX, primaryY, secondaryX, secondaryY, opt_onHide); @@ -340,10 +377,11 @@ Blockly.DropDownDiv.show = function(owner, rtl, primaryX, primaryY, var div = Blockly.DropDownDiv.DIV_; div.style.direction = rtl ? 'rtl' : 'ltr'; + var mainWorkspace = + /** @type {!Blockly.WorkspaceSvg} */ (Blockly.getMainWorkspace()); Blockly.DropDownDiv.rendererClassName_ = - Blockly.getMainWorkspace().getRenderer().getClassName(); - Blockly.DropDownDiv.themeClassName_ = - Blockly.getMainWorkspace().getTheme().getClassName(); + mainWorkspace.getRenderer().getClassName(); + Blockly.DropDownDiv.themeClassName_ = mainWorkspace.getTheme().getClassName(); Blockly.utils.dom.addClass(div, Blockly.DropDownDiv.rendererClassName_); Blockly.utils.dom.addClass(div, Blockly.DropDownDiv.themeClassName_); @@ -362,8 +400,8 @@ Blockly.DropDownDiv.show = function(owner, rtl, primaryX, primaryY, /** * Get sizing info about the bounding element. - * @return {!Object} An object containing size information about the bounding - * element (bounding box and width/height). + * @return {!Blockly.DropDownDiv.BoundsInfo} An object containing size + * information about the bounding element (bounding box and width/height). * @private */ Blockly.DropDownDiv.getBoundsInfo_ = function() { @@ -388,11 +426,11 @@ Blockly.DropDownDiv.getBoundsInfo_ = function() { * @param {number} primaryX Desired origin point x, in absolute px. * @param {number} primaryY Desired origin point y, in absolute px. * @param {number} secondaryX Secondary/alternative origin point x, - * in absolute px. + * in absolute px. * @param {number} secondaryY Secondary/alternative origin point y, - * in absolute px. - * @return {Object} Various final metrics, including rendered positions - * for drop-down and arrow. + * in absolute px. + * @return {!Blockly.DropDownDiv.PositionMetrics} Various final metrics, + * including rendered positions for drop-down and arrow. * @private */ Blockly.DropDownDiv.getPositionMetrics_ = function(primaryX, primaryY, @@ -431,12 +469,12 @@ Blockly.DropDownDiv.getPositionMetrics_ = function(primaryX, primaryY, * Get the metrics for positioning the div below the source. * @param {number} primaryX Desired origin point x, in absolute px. * @param {number} primaryY Desired origin point y, in absolute px. - * @param {!Object} boundsInfo An object containing size information about the - * bounding element (bounding box and width/height). - * @param {!Object} divSize An object containing information about the size - * of the DropDownDiv (width & height). - * @return {Object} Various final metrics, including rendered positions - * for drop-down and arrow. + * @param {!Blockly.DropDownDiv.BoundsInfo} boundsInfo An object containing size + * information about the bounding element (bounding box and width/height). + * @param {!Blockly.utils.Size} divSize An object containing information about + * the size of the DropDownDiv (width & height). + * @return {!Blockly.DropDownDiv.PositionMetrics} Various final metrics, + * including rendered positions for drop-down and arrow. * @private */ Blockly.DropDownDiv.getPositionBelowMetrics_ = function( @@ -464,15 +502,15 @@ Blockly.DropDownDiv.getPositionBelowMetrics_ = function( /** * Get the metrics for positioning the div above the source. * @param {number} secondaryX Secondary/alternative origin point x, - * in absolute px. + * in absolute px. * @param {number} secondaryY Secondary/alternative origin point y, - * in absolute px. - * @param {!Object} boundsInfo An object containing size information about the - * bounding element (bounding box and width/height). - * @param {!Object} divSize An object containing information about the size - * of the DropDownDiv (width & height). - * @return {Object} Various final metrics, including rendered positions - * for drop-down and arrow. + * in absolute px. + * @param {!Blockly.DropDownDiv.BoundsInfo} boundsInfo An object containing size + * information about the bounding element (bounding box and width/height). + * @param {!Blockly.utils.Size} divSize An object containing information about + * the size of the DropDownDiv (width & height). + * @return {!Blockly.DropDownDiv.PositionMetrics} Various final metrics, + * including rendered positions for drop-down and arrow. * @private */ Blockly.DropDownDiv.getPositionAboveMetrics_ = function( @@ -501,12 +539,12 @@ Blockly.DropDownDiv.getPositionAboveMetrics_ = function( /** * Get the metrics for positioning the div at the top of the page. * @param {number} sourceX Desired origin point x, in absolute px. - * @param {!Object} boundsInfo An object containing size information about the - * bounding element (bounding box and width/height). - * @param {!Object} divSize An object containing information about the size - * of the DropDownDiv (width & height). - * @return {Object} Various final metrics, including rendered positions - * for drop-down and arrow. + * @param {!Blockly.DropDownDiv.BoundsInfo} boundsInfo An object containing size + * information about the bounding element (bounding box and width/height). + * @param {!Blockly.utils.Size} divSize An object containing information about + * the size of the DropDownDiv (width & height). + * @return {!Blockly.DropDownDiv.PositionMetrics} Various final metrics, + * including rendered positions for drop-down and arrow. * @private */ Blockly.DropDownDiv.getPositionTopOfPageMetrics_ = function( @@ -521,6 +559,9 @@ Blockly.DropDownDiv.getPositionTopOfPageMetrics_ = function( initialY : 0, finalX: xCoords.divX, // X position remains constant during animation. finalY: 0, // Y position remains constant during animation. + arrowAtTop: null, + arrowX: null, + arrowY: null, arrowVisible: false }; }; @@ -649,7 +690,8 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { Blockly.utils.dom.removeClass(div, Blockly.DropDownDiv.themeClassName_); Blockly.DropDownDiv.themeClassName_ = ''; } - Blockly.getMainWorkspace().markFocused(); + (/** @type {!Blockly.WorkspaceSvg} */ ( + Blockly.getMainWorkspace())).markFocused(); }; /** @@ -700,7 +742,7 @@ Blockly.DropDownDiv.positionInternal_ = function( var dy = finalY - initialY; div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)'; - return metrics.arrowAtTop; + return !!metrics.arrowAtTop; }; /** @@ -716,7 +758,7 @@ Blockly.DropDownDiv.repositionForWindowResize = function() { // it. if (Blockly.DropDownDiv.owner_) { var field = /** @type {!Blockly.Field} */ (Blockly.DropDownDiv.owner_); - var block = Blockly.DropDownDiv.owner_.getSourceBlock(); + var block = /** @type {!Blockly.BlockSvg} */ (field.getSourceBlock()); var bBox = Blockly.DropDownDiv.positionToField_ ? Blockly.DropDownDiv.getScaledBboxOfField_(field) : Blockly.DropDownDiv.getScaledBboxOfBlock_(block); diff --git a/core/field.js b/core/field.js index 2c6328fc8..d1e501cc3 100644 --- a/core/field.js +++ b/core/field.js @@ -19,11 +19,16 @@ goog.require('Blockly.Events.BlockChange'); goog.require('Blockly.Gesture'); goog.require('Blockly.utils'); goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.Rect'); goog.require('Blockly.utils.Size'); goog.require('Blockly.utils.style'); goog.require('Blockly.utils.userAgent'); goog.requireType('Blockly.blockRendering.ConstantProvider'); +goog.requireType('Blockly.IASTNodeLocationSvg'); +goog.requireType('Blockly.IASTNodeLocationWithBlock'); +goog.requireType('Blockly.IBlocklyActionable'); +goog.requireType('Blockly.IRegistrable'); /** @@ -36,6 +41,10 @@ goog.requireType('Blockly.blockRendering.ConstantProvider'); * the individual field's documentation for a list of properties this * parameter supports. * @constructor + * @implements {Blockly.IASTNodeLocationSvg} + * @implements {Blockly.IASTNodeLocationWithBlock} + * @implements {Blockly.IBlocklyActionable} + * @implements {Blockly.IRegistrable} */ Blockly.Field = function(value, opt_validator, opt_config) { /** @@ -44,7 +53,7 @@ Blockly.Field = function(value, opt_validator, opt_config) { * @type {*} * @protected */ - this.value_ = null; + this.value_ = this.DEFAULT_VALUE; /** * Validation function called when user edits an editable field. @@ -131,6 +140,13 @@ Blockly.Field = function(value, opt_validator, opt_config) { opt_validator && this.setValidator(opt_validator); }; +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.Field.prototype.DEFAULT_VALUE = null; + /** * Name of field. Unique within each block. * Static labels are usually unnamed. @@ -708,8 +724,8 @@ Blockly.Field.prototype.getSize = function() { /** * Returns the bounding box of the rendered field, accounting for workspace * scaling. - * @return {!Object} An object with top, bottom, left, and right in pixels - * relative to the top left corner of the page (window coordinates). + * @return {!Blockly.utils.Rect} An object with top, bottom, left, and right in + * pixels relative to the top left corner of the page (window coordinates). * @package */ Blockly.Field.prototype.getScaledBBox = function() { @@ -742,12 +758,12 @@ Blockly.Field.prototype.getScaledBBox = function() { var scaledWidth = bBox.width; var scaledHeight = bBox.height; } - return { - top: xy.y, - bottom: xy.y + scaledHeight, - left: xy.x, - right: xy.x + scaledWidth - }; + return new Blockly.utils.Rect( + xy.y, + xy.y + scaledHeight, + xy.x, + xy.x + scaledWidth + ); }; /** @@ -858,16 +874,20 @@ Blockly.Field.prototype.setValue = function(newValue) { return; } } + var source = this.sourceBlock_; + if (source && source.disposed) { + doLogging && console.log('source disposed, return'); + return; + } var oldValue = this.getValue(); if (oldValue === newValue) { doLogging && console.log('same, return'); - // No change. return; } - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + if (source && Blockly.Events.isEnabled()) { Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name || null, oldValue, newValue)); + source, 'field', this.name || null, oldValue, newValue)); } this.doValueUpdate_(newValue); if (this.isDirty_) { @@ -1101,7 +1121,8 @@ Blockly.Field.prototype.setMarkerSvg = function(markerSvg) { * @protected */ Blockly.Field.prototype.updateMarkers_ = function() { - var workspace = this.sourceBlock_.workspace; + var workspace = + /** @type {!Blockly.WorkspaceSvg} */ (this.sourceBlock_.workspace); if (workspace.keyboardAccessibilityMode && this.cursorSvg_) { workspace.getCursor().draw(); } diff --git a/core/field_angle.js b/core/field_angle.js index 4dbe1b19c..04133dbf4 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -71,7 +71,7 @@ Blockly.FieldAngle = function(opt_value, opt_validator, opt_config) { this.round_ = Blockly.FieldAngle.ROUND; Blockly.FieldAngle.superClass_.constructor.call( - this, opt_value || 0, opt_validator, opt_config); + this, opt_value, opt_validator, opt_config); /** * The angle picker's gauge path depending on the value. @@ -108,6 +108,14 @@ Blockly.FieldAngle = function(opt_value, opt_validator, opt_config) { }; Blockly.utils.object.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); + +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldAngle.prototype.DEFAULT_VALUE = 0; + /** * Construct a FieldAngle from a JSON arg object. * @param {!Object} options A JSON object with options (angle). @@ -247,7 +255,7 @@ Blockly.FieldAngle.prototype.render_ = function() { * Create and show the angle field's editor. * @param {Event=} opt_e Optional mouse event that triggered the field to open, * or undefined if triggered programmatically. - * @private + * @protected */ Blockly.FieldAngle.prototype.showEditor_ = function(opt_e) { // Mobile browsers have issues with in-line textareas (focus & keyboards). diff --git a/core/field_checkbox.js b/core/field_checkbox.js index e4818ebe8..acca4f203 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -44,14 +44,18 @@ Blockly.FieldCheckbox = function(opt_value, opt_validator, opt_config) { */ this.checkChar_ = null; - if (opt_value == null) { - opt_value = 'FALSE'; - } Blockly.FieldCheckbox.superClass_.constructor.call( this, opt_value, opt_validator, opt_config); }; Blockly.utils.object.inherits(Blockly.FieldCheckbox, Blockly.Field); +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldCheckbox.prototype.DEFAULT_VALUE = false; + /** * Construct a FieldCheckbox from a JSON arg object. * @param {!Object} options A JSON object with options (checked). diff --git a/core/field_colour.js b/core/field_colour.js index 562bdaeec..fbef49384 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -43,8 +43,7 @@ goog.require('Blockly.utils.Size'); */ Blockly.FieldColour = function(opt_value, opt_validator, opt_config) { Blockly.FieldColour.superClass_.constructor.call( - this, opt_value || Blockly.FieldColour.COLOURS[0], - opt_validator, opt_config); + this, opt_value, opt_validator, opt_config); /** * The field's colour picker element. @@ -268,6 +267,13 @@ Blockly.FieldColour.COLOURS = [ '#ffccff', '#ff99ff', '#cc66cc', '#cc33cc', '#993399', '#663366', '#330033' ]; +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldColour.prototype.DEFAULT_VALUE = Blockly.FieldColour.COLOURS[0]; + /** * An array of tooltip strings for the palette. If not the same length as * COLOURS, the colour's hex code will be used for any missing titles. @@ -311,7 +317,7 @@ Blockly.FieldColour.prototype.setColumns = function(columns) { /** * Create and show the colour field's editor. - * @private + * @protected */ Blockly.FieldColour.prototype.showEditor_ = function() { this.picker_ = this.dropdownCreate_(); diff --git a/core/field_date.js b/core/field_date.js deleted file mode 100644 index aaed6bfa4..000000000 --- a/core/field_date.js +++ /dev/null @@ -1,344 +0,0 @@ -/** - * @license - * Copyright 2015 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Date input field. - * @author pkendall64@gmail.com (Paul Kendall) - */ -'use strict'; - -goog.provide('Blockly.FieldDate'); - -goog.require('Blockly.Css'); -goog.require('Blockly.Events'); -goog.require('Blockly.Field'); -goog.require('Blockly.fieldRegistry'); -goog.require('Blockly.utils.dom'); -goog.require('Blockly.utils.object'); -goog.require('Blockly.utils.string'); - -goog.require('goog.date'); -goog.require('goog.date.DateTime'); -goog.require('goog.events'); -goog.require('goog.i18n.DateTimeSymbols'); -goog.require('goog.i18n.DateTimeSymbols_he'); -goog.require('goog.ui.DatePicker'); - - -/** - * Class for a date input field. - * @param {string=} opt_value The initial value of the field. Should be in - * 'YYYY-MM-DD' format. Defaults to the current date. - * @param {Function=} opt_validator A function that is called to validate - * changes to the field's value. Takes in a date string & returns a - * validated date string ('YYYY-MM-DD' format), or null to abort the change. - * @extends {Blockly.Field} - * @constructor - */ -Blockly.FieldDate = function(opt_value, opt_validator) { - Blockly.FieldDate.superClass_.constructor.call(this, - opt_value || new goog.date.Date().toIsoString(true), opt_validator); -}; -Blockly.utils.object.inherits(Blockly.FieldDate, Blockly.Field); - -/** - * Construct a FieldDate from a JSON arg object. - * @param {!Object} options A JSON object with options (date). - * @return {!Blockly.FieldDate} The new field instance. - * @package - * @nocollapse - */ -Blockly.FieldDate.fromJson = function(options) { - return new Blockly.FieldDate(options['date']); -}; - -/** - * Serializable fields are saved by the XML renderer, non-serializable fields - * are not. Editable fields should also be serializable. - * @type {boolean} - */ -Blockly.FieldDate.prototype.SERIALIZABLE = true; - -/** - * Mouse cursor style when over the hotspot that initiates the editor. - */ -Blockly.FieldDate.prototype.CURSOR = 'text'; - -/** - * Border colour for the dropdown div showing the date picker. Must be a CSS - * string. - * @type {string} - * @private - */ -Blockly.FieldDate.prototype.DROPDOWN_BORDER_COLOUR = 'silver'; - -/** - * Background colour for the dropdown div showing the date picker. Must be a - * CSS string. - * @type {string} - * @private - */ -Blockly.FieldDate.prototype.DROPDOWN_BACKGROUND_COLOUR = 'white'; - -/** - * Ensure that the input value is a valid date. - * @param {*=} opt_newValue The input value. - * @return {?string} A valid date, or null if invalid. - * @protected - */ -Blockly.FieldDate.prototype.doClassValidation_ = function(opt_newValue) { - if (!opt_newValue) { - return null; - } - // Check if the new value is parsable or not. - var date = goog.date.Date.fromIsoString(opt_newValue); - if (!date || date.toIsoString(true) != opt_newValue) { - return null; - } - return opt_newValue; -}; - -/** - * Render the field. If the picker is shown make sure it has the current - * date selected. - * @protected - */ -Blockly.FieldDate.prototype.render_ = function() { - Blockly.FieldDate.superClass_.render_.call(this); - if (this.picker_) { - this.picker_.setDate(goog.date.Date.fromIsoString(this.getValue())); - this.updateEditor_(); - } -}; - -/** - * Updates the field's colours to match those of the block. - * @package - */ -Blockly.FieldDate.prototype.applyColour = function() { - this.todayColour_ = this.sourceBlock_.style.colourPrimary; - this.selectedColour_ = this.sourceBlock_.style.colourSecondary; - this.updateEditor_(); -}; - -/** - * Updates the picker to show the current date and currently selected date. - * @private - */ -Blockly.FieldDate.prototype.updateEditor_ = function() { - if (!this.picker_) { - // Nothing to update. - return; - } - - // Updating today should come before updating selected, so that if the - // current day is selected, it will appear so. - if (this.oldTodayElement_) { - this.oldTodayElement_.style.backgroundColor = null; - this.oldTodayElement_.style.color = null; - } - var today = this.picker_.getElementByClass('goog-date-picker-today'); - this.oldTodayElement_ = today; - if (today) { - today.style.backgroundColor = this.todayColour_; - today.style.color = 'white'; - } - - if (this.oldSelectedElement_ && this.oldSelectedElement_ != today) { - this.oldSelectedElement_.style.backgroundColor = null; - this.oldSelectedElement_.style.color = null; - } - var selected = this.picker_.getElementByClass('goog-date-picker-selected'); - this.oldSelectedElement_ = selected; - if (selected) { - selected.style.backgroundColor = this.selectedColour_; - selected.style.color = this.todayColour_; - } -}; - -/** - * Create and show the date field's editor. - * @private - */ -Blockly.FieldDate.prototype.showEditor_ = function() { - this.picker_ = this.dropdownCreate_(); - this.picker_.render(Blockly.DropDownDiv.getContentDiv()); - Blockly.utils.dom.addClass(this.picker_.getElement(), 'blocklyDatePicker'); - Blockly.DropDownDiv.setColour( - this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR); - - Blockly.DropDownDiv.showPositionedByField( - this, this.dropdownDispose_.bind(this)); - - this.updateEditor_(); -}; - -/** - * Create the date dropdown editor. - * @return {!goog.ui.DatePicker} The newly created date picker. - * @private - */ -Blockly.FieldDate.prototype.dropdownCreate_ = function() { - // Create the date picker using Closure. - Blockly.FieldDate.loadLanguage_(); - var picker = new goog.ui.DatePicker(); - picker.setAllowNone(false); - picker.setShowWeekNum(false); - picker.setUseNarrowWeekdayNames(true); - picker.setUseSimpleNavigationMenu(true); - picker.setDate(goog.date.DateTime.fromIsoString(this.getValue())); - - this.changeEventKey_ = goog.events.listen( - picker, - goog.ui.DatePicker.Events.CHANGE, - this.onDateSelected_, - null, - this); - this.activeMonthEventKey_ = goog.events.listen( - picker, - goog.ui.DatePicker.Events.CHANGE_ACTIVE_MONTH, - this.updateEditor_, - null, - this); - - return picker; -}; - -/** - * Dispose of references to DOM elements and events belonging - * to the date editor. - * @private - */ -Blockly.FieldDate.prototype.dropdownDispose_ = function() { - goog.events.unlistenByKey(this.changeEventKey_); - goog.events.unlistenByKey(this.activeMonthEventKey_); -}; - -/** - * Handle a CHANGE event in the date picker. - * @param {!Event} event The CHANGE event. - * @private - */ -Blockly.FieldDate.prototype.onDateSelected_ = function(event) { - var date = event.date ? event.date.toIsoString(true) : ''; - this.setValue(date); - Blockly.DropDownDiv.hideIfOwner(this); -}; - -/** - * Load the best language pack by scanning the Blockly.Msg object for a - * language that matches the available languages in Closure. - * @private - */ -Blockly.FieldDate.loadLanguage_ = function() { - for (var prop in goog.i18n) { - if (Blockly.utils.string.startsWith(prop, 'DateTimeSymbols_')) { - var lang = prop.substr(16).toLowerCase().replace('_', '.'); - // E.g. 'DateTimeSymbols_pt_BR' -> 'pt.br' - if (goog.getObjectByName(lang, Blockly.Msg)) { - goog.i18n.DateTimeSymbols = goog.i18n[prop]; - break; - } - } - } -}; - -/** - * CSS for date picker. See css.js for use. - */ -Blockly.Css.register([ - /* eslint-disable indent */ - '.blocklyDatePicker,', - '.blocklyDatePicker th,', - '.blocklyDatePicker td {', - 'font: 13px Arial, sans-serif;', - 'color: #3c4043;', - '}', - - '.blocklyDatePicker th,', - '.blocklyDatePicker td {', - 'text-align: center;', - 'vertical-align: middle;', - '}', - - '.blocklyDatePicker .goog-date-picker-wday,', - '.blocklyDatePicker .goog-date-picker-date {', - 'padding: 6px 6px;', - '}', - - '.blocklyDatePicker button {', - 'cursor: pointer;', - 'padding: 6px 6px;', - 'margin: 1px 0;', - 'border: 0;', - 'color: #3c4043;', - 'font-weight: bold;', - 'background: transparent;', - '}', - - '.blocklyDatePicker .goog-date-picker-previousMonth,', - '.blocklyDatePicker .goog-date-picker-nextMonth {', - 'height: 24px;', - 'width: 24px;', - '}', - - '.blocklyDatePicker .goog-date-picker-monthyear {', - 'font-weight: bold;', - '}', - - '.blocklyDatePicker .goog-date-picker-wday, ', - '.blocklyDatePicker .goog-date-picker-other-month {', - 'color: #70757a;', - 'border-radius: 12px;', - '}', - - '.blocklyDatePicker button,', - '.blocklyDatePicker .goog-date-picker-date {', - 'cursor: pointer;', - 'background-color: rgb(218, 220, 224, 0);', - 'border-radius: 12px;', - 'transition: background-color,opacity 100ms linear;', - '}', - - '.blocklyDatePicker button:hover,', - '.blocklyDatePicker .goog-date-picker-date:hover {', - 'background-color: rgb(218, 220, 224, .5);', - '}' - /* eslint-enable indent */ -]); - -Blockly.fieldRegistry.register('field_date', Blockly.FieldDate); - - -/** - * Back up original getMsg function. - * @type {!Function} - */ -goog.getMsgOrig = goog.getMsg; - -/** - * Gets a localized message. - * Overrides the default Closure function to check for a Blockly.Msg first. - * Used infrequently, only known case is TODAY button in date picker. - * @param {string} str Translatable string, places holders in the form {$foo}. - * @param {Object.=} opt_values Maps place holder name to value. - * @return {string} Message with placeholders filled. - * @suppress {duplicate} - */ -goog.getMsg = function(str, opt_values) { - var key = goog.getMsg.blocklyMsgMap[str]; - if (key) { - str = Blockly.Msg[key]; - } - return goog.getMsgOrig(str, opt_values); -}; - -/** - * Mapping of Closure messages to Blockly.Msg names. - */ -goog.getMsg.blocklyMsgMap = { - 'Today': 'TODAY' -}; diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 87062c5a4..bdbe27f9e 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -67,6 +67,20 @@ Blockly.FieldDropdown = function(menuGenerator, opt_validator, opt_config) { */ this.generatedOptions_ = null; + /** + * The prefix field label, of common words set after options are trimmed. + * @type {?string} + * @package + */ + this.prefixField = null; + + /** + * The suffix field label, of common words set after options are trimmed. + * @type {?string} + * @package + */ + this.suffixField = null; + this.trimOptions_(); /** @@ -258,7 +272,7 @@ Blockly.FieldDropdown.prototype.createSVGArrow_ = function() { * Create a dropdown menu under the text. * @param {Event=} opt_e Optional mouse event that triggered the field to open, * or undefined if triggered programmatically. - * @private + * @protected */ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) { this.menu_ = this.dropdownCreate_(); @@ -270,8 +284,8 @@ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) { } // Element gets created in render. this.menu_.render(Blockly.DropDownDiv.getContentDiv()); - Blockly.utils.dom.addClass( - /** @type {!Element} */ (this.menu_.getElement()), 'blocklyDropdownMenu'); + var menuElement = /** @type {!Element} */ (this.menu_.getElement()); + Blockly.utils.dom.addClass(menuElement, 'blocklyDropdownMenu'); if (this.getConstants().FIELD_DROPDOWN_COLOURED_DIV) { var primaryColour = (this.sourceBlock_.isShadow()) ? @@ -291,11 +305,8 @@ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) { // view. See issue #1329. this.menu_.focus(); - // Scroll the dropdown to show the selected menu item. if (this.selectedMenuItem_) { - Blockly.utils.style.scrollIntoContainerView( - /** @type {!Element} */ (this.selectedMenuItem_.getElement()), - /** @type {!Element} */ (this.menu_.getElement())); + this.menu_.setHighlighted(this.selectedMenuItem_); } this.applyColour(); @@ -308,7 +319,6 @@ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) { */ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() { var menu = new Blockly.Menu(); - menu.setRightToLeft(this.sourceBlock_.RTL); menu.setRole(Blockly.utils.aria.Role.LISTBOX); var options = this.getOptions(false); @@ -323,12 +333,11 @@ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() { image.alt = content['alt'] || ''; content = image; } - var menuItem = new Blockly.MenuItem(content); + var menuItem = new Blockly.MenuItem(content, value); menuItem.setRole(Blockly.utils.aria.Role.OPTION); menuItem.setRightToLeft(this.sourceBlock_.RTL); - menuItem.setValue(value); menuItem.setCheckable(true); - menu.addChild(menuItem, true); + menu.addChild(menuItem); menuItem.setChecked(value == this.value_); if (value == this.value_) { this.selectedMenuItem_ = menuItem; @@ -336,10 +345,6 @@ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() { menuItem.onAction(this.handleMenuActionEvent_, this); } - Blockly.utils.aria.setState(/** @type {!Element} */ (menu.getElement()), - Blockly.utils.aria.State.ACTIVEDESCENDANT, - this.selectedMenuItem_ ? this.selectedMenuItem_.getId() : ''); - return menu; }; @@ -382,8 +387,6 @@ Blockly.FieldDropdown.prototype.onItemSelected_ = function(menu, menuItem) { * @private */ Blockly.FieldDropdown.prototype.trimOptions_ = function() { - this.prefixField = null; - this.suffixField = null; var options = this.menuGenerator_; if (!Array.isArray(options)) { return; @@ -549,7 +552,7 @@ Blockly.FieldDropdown.prototype.applyColour = function() { /** * Draws the border with the correct width. - * @private + * @protected */ Blockly.FieldDropdown.prototype.render_ = function() { // Hide both elements. diff --git a/core/field_image.js b/core/field_image.js index 32af8adca..d6474ea3b 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -22,7 +22,7 @@ goog.require('Blockly.utils.Size'); /** * Class for an image on a block. - * @param {string} src The URL of the image. Defaults to an empty string. + * @param {string} src The URL of the image. * @param {!(string|number)} width Width of the image. * @param {!(string|number)} height Height of the image. * @param {string=} opt_alt Optional alt text for when block is collapsed. @@ -70,7 +70,7 @@ Blockly.FieldImage = function(src, width, height, this.altText_ = ''; Blockly.FieldImage.superClass_.constructor.call( - this, src || '', null, opt_config); + this, src, null, opt_config); if (!opt_config) { // If the config wasn't passed, do old configuration. this.flipRtl_ = !!opt_flipRtl; @@ -114,6 +114,13 @@ Blockly.FieldImage = function(src, width, height, }; Blockly.utils.object.inherits(Blockly.FieldImage, Blockly.Field); +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldImage.prototype.DEFAULT_VALUE = ''; + /** * Construct a FieldImage from a JSON arg object, * dereferencing any string table references. diff --git a/core/field_label.js b/core/field_label.js index 666e2ec78..87942a34b 100644 --- a/core/field_label.js +++ b/core/field_label.js @@ -40,9 +40,6 @@ Blockly.FieldLabel = function(opt_value, opt_class, opt_config) { */ this.class_ = null; - if (opt_value == null) { - opt_value = ''; - } Blockly.FieldLabel.superClass_.constructor.call( this, opt_value, null, opt_config); @@ -52,6 +49,13 @@ Blockly.FieldLabel = function(opt_value, opt_class, opt_config) { }; Blockly.utils.object.inherits(Blockly.FieldLabel, Blockly.Field); +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldLabel.prototype.DEFAULT_VALUE = ''; + /** * Construct a FieldLabel from a JSON arg object, * dereferencing any string table references. diff --git a/core/field_multilineinput.js b/core/field_multilineinput.js index 734f4135a..0660aa571 100644 --- a/core/field_multilineinput.js +++ b/core/field_multilineinput.js @@ -43,9 +43,6 @@ goog.require('Blockly.utils.userAgent'); Blockly.FieldMultilineInput = function(opt_value, opt_validator, opt_config) { // TODO: Once this field is documented the opt_config link should point to its // creation documentation, rather than the text input field's. - if (opt_value == null) { - opt_value = ''; - } Blockly.FieldMultilineInput.superClass_.constructor.call(this, opt_value, opt_validator, opt_config); @@ -59,7 +56,6 @@ Blockly.FieldMultilineInput = function(opt_value, opt_validator, opt_config) { Blockly.utils.object.inherits(Blockly.FieldMultilineInput, Blockly.FieldTextInput); - /** * Construct a FieldMultilineInput from a JSON arg object, * dereferencing any string table references. diff --git a/core/field_number.js b/core/field_number.js index 8e49a2abe..061995348 100644 --- a/core/field_number.js +++ b/core/field_number.js @@ -67,7 +67,7 @@ Blockly.FieldNumber = function(opt_value, opt_min, opt_max, opt_precision, this.decimalPlaces_ = null; Blockly.FieldNumber.superClass_.constructor.call( - this, opt_value || 0, opt_validator, opt_config); + this, opt_value, opt_validator, opt_config); if (!opt_config) { // Only do one kind of configuration or the other. this.setConstraints(opt_min, opt_max, opt_precision); @@ -75,6 +75,13 @@ Blockly.FieldNumber = function(opt_value, opt_min, opt_max, opt_precision, }; Blockly.utils.object.inherits(Blockly.FieldNumber, Blockly.FieldTextInput); +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldNumber.prototype.DEFAULT_VALUE = 0; + /** * Construct a FieldNumber from a JSON arg object. * @param {!Object} options A JSON object with options (value, min, max, and diff --git a/core/field_registry.js b/core/field_registry.js index 5e6b7bde0..b7f8441b3 100644 --- a/core/field_registry.js +++ b/core/field_registry.js @@ -14,39 +14,23 @@ goog.provide('Blockly.fieldRegistry'); +goog.require('Blockly.registry'); -/** - * The set of all registered fields, keyed by field type as used in the JSON - * definition of a block. - * @type {!Object} - * @private - */ -Blockly.fieldRegistry.typeMap_ = {}; /** * Registers a field type. * Blockly.fieldRegistry.fromJson uses this registry to * find the appropriate field type. * @param {string} type The field type name as used in the JSON definition. - * @param {!{fromJson: Function}} fieldClass The field class containing a - * fromJson function that can construct an instance of the field. + * @param {?function(new:Blockly.Field, ...?)} fieldClass The field class + * containing a fromJson function that can construct an instance of the + * field. * @throws {Error} if the type name is empty, the field is already * registered, or the fieldClass is not an object containing a fromJson * function. */ Blockly.fieldRegistry.register = function(type, fieldClass) { - if ((typeof type != 'string') || (type.trim() == '')) { - throw Error('Invalid field type "' + type + '". The type must be a' + - ' non-empty string.'); - } - if (Blockly.fieldRegistry.typeMap_[type]) { - throw Error('Error: Field "' + type + '" is already registered.'); - } - if (!fieldClass || (typeof fieldClass.fromJson != 'function')) { - throw Error('Field "' + fieldClass + '" must have a fromJson function'); - } - type = type.toLowerCase(); - Blockly.fieldRegistry.typeMap_[type] = fieldClass; + Blockly.registry.register(Blockly.registry.Type.FIELD, type, fieldClass); }; /** @@ -54,12 +38,7 @@ Blockly.fieldRegistry.register = function(type, fieldClass) { * @param {string} type The field type name as used in the JSON definition. */ Blockly.fieldRegistry.unregister = function(type) { - if (Blockly.fieldRegistry.typeMap_[type]) { - delete Blockly.fieldRegistry.typeMap_[type]; - } else { - console.warn('No field mapping for type "' + type + - '" found to unregister'); - } + Blockly.registry.unregister(Blockly.registry.Type.FIELD, type); }; /** @@ -73,8 +52,8 @@ Blockly.fieldRegistry.unregister = function(type) { * @package */ Blockly.fieldRegistry.fromJson = function(options) { - var type = options['type'].toLowerCase(); - var fieldClass = Blockly.fieldRegistry.typeMap_[type]; + var fieldClass = /** @type {{fromJson:function(!Object):!Blockly.Field}} */ ( + Blockly.registry.getClass(Blockly.registry.Type.FIELD, options['type'])); if (!fieldClass) { console.warn('Blockly could not create a field of type ' + options['type'] + '. The field is probably not being registered. This could be because' + diff --git a/core/field_textinput.js b/core/field_textinput.js index 2343a2232..4bd367f37 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -48,9 +48,6 @@ Blockly.FieldTextInput = function(opt_value, opt_validator, opt_config) { */ this.spellcheck_ = true; - if (opt_value == null) { - opt_value = ''; - } Blockly.FieldTextInput.superClass_.constructor.call(this, opt_value, opt_validator, opt_config); @@ -80,9 +77,23 @@ Blockly.FieldTextInput = function(opt_value, opt_validator, opt_config) { * @type {?boolean} */ this.fullBlockClickTarget_ = false; + + /** + * The workspace that this field belongs to. + * @type {?Blockly.WorkspaceSvg} + * @protected + */ + this.workspace_ = null; }; Blockly.utils.object.inherits(Blockly.FieldTextInput, Blockly.Field); +/** + * The default value for this field. + * @type {*} + * @protected + */ +Blockly.FieldTextInput.prototype.DEFAULT_VALUE = ''; + /** * Construct a FieldTextInput from a JSON arg object, * dereferencing any string table references. @@ -275,7 +286,8 @@ Blockly.FieldTextInput.prototype.setSpellcheck = function(check) { */ Blockly.FieldTextInput.prototype.showEditor_ = function(_opt_e, opt_quietInput) { - this.workspace_ = this.sourceBlock_.workspace; + this.workspace_ = + (/** @type {!Blockly.BlockSvg} */ (this.sourceBlock_)).workspace; var quietInput = opt_quietInput || false; if (!quietInput && (Blockly.utils.userAgent.MOBILE || Blockly.utils.userAgent.ANDROID || diff --git a/core/field_variable.js b/core/field_variable.js index 7d7801384..df4cd526a 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -98,13 +98,6 @@ Blockly.FieldVariable.fromJson = function(options) { varName, undefined, undefined, undefined, options); }; -/** - * The workspace that this variable field belongs to. - * @type {?Blockly.Workspace} - * @private - */ -Blockly.FieldVariable.prototype.workspace_ = null; - /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. diff --git a/core/flyout_base.js b/core/flyout_base.js index 5d0628700..547035ad3 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -29,15 +29,24 @@ goog.require('Blockly.utils.dom'); goog.require('Blockly.WorkspaceSvg'); goog.require('Blockly.Xml'); +goog.requireType('Blockly.IBlocklyActionable'); +goog.requireType('Blockly.IDeleteArea'); +goog.requireType('Blockly.utils.Metrics'); + /** * Class for a flyout. * @param {!Blockly.Options} workspaceOptions Dictionary of options for the * workspace. * @constructor + * @abstract + * @implements {Blockly.IBlocklyActionable} + * @implements {Blockly.IDeleteArea} */ Blockly.Flyout = function(workspaceOptions) { - workspaceOptions.getMetrics = this.getMetrics_.bind(this); + workspaceOptions.getMetrics = + /** @type {function():!Blockly.utils.Metrics} */ ( + this.getMetrics_.bind(this)); workspaceOptions.setMetrics = this.setMetrics_.bind(this); /** @@ -55,6 +64,13 @@ Blockly.Flyout = function(workspaceOptions) { */ this.RTL = !!workspaceOptions.RTL; + /** + * Whether the flyout should be laid out horizontally or not. + * @type {boolean} + * @package + */ + this.horizontalLayout = false; + /** * Position of the toolbox and flyout relative to the workspace. * @type {number} @@ -105,6 +121,13 @@ Blockly.Flyout = function(workspaceOptions) { * @const */ this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH; + + /** + * The target workspace + * @type {?Blockly.WorkspaceSvg} + * @package + */ + this.targetWorkspace = null; }; /** @@ -232,12 +255,15 @@ Blockly.Flyout.prototype.createDom = function(tagName) { * create new blocks. */ Blockly.Flyout.prototype.init = function(targetWorkspace) { - this.targetWorkspace_ = targetWorkspace; + this.targetWorkspace = targetWorkspace; this.workspace_.targetWorkspace = targetWorkspace; - // Add scrollbar. - this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, - this.horizontalLayout_, false, 'blocklyFlyoutScrollbar'); + /** + * @type {!Blockly.Scrollbar} + * @package + */ + this.scrollbar = new Blockly.Scrollbar(this.workspace_, + this.horizontalLayout, false, 'blocklyFlyoutScrollbar'); this.hide(); @@ -245,7 +271,7 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) { Blockly.bindEventWithChecks_(this.svgGroup_, 'wheel', this, this.wheel_)); if (!this.autoClose) { this.filterWrapper_ = this.filterForCapacity_.bind(this); - this.targetWorkspace_.addChangeListener(this.filterWrapper_); + this.targetWorkspace.addChangeListener(this.filterWrapper_); } // Dragging the flyout up and down. @@ -255,10 +281,10 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) { // A flyout connected to a workspace doesn't have its own current gesture. this.workspace_.getGesture = - this.targetWorkspace_.getGesture.bind(this.targetWorkspace_); + this.targetWorkspace.getGesture.bind(this.targetWorkspace); // Get variables from the main workspace rather than the target workspace. - this.workspace_.setVariableMap(this.targetWorkspace_.getVariableMap()); + this.workspace_.setVariableMap(this.targetWorkspace.getVariableMap()); this.workspace_.createPotentialVariableMap(); }; @@ -272,12 +298,12 @@ Blockly.Flyout.prototype.dispose = function() { this.hide(); Blockly.unbindEvent_(this.eventWrappers_); if (this.filterWrapper_) { - this.targetWorkspace_.removeChangeListener(this.filterWrapper_); + this.targetWorkspace.removeChangeListener(this.filterWrapper_); this.filterWrapper_ = null; } - if (this.scrollbar_) { - this.scrollbar_.dispose(); - this.scrollbar_ = null; + if (this.scrollbar) { + this.scrollbar.dispose(); + this.scrollbar = null; } if (this.workspace_) { this.workspace_.getThemeManager().unsubscribe(this.svgBackground_); @@ -290,7 +316,7 @@ Blockly.Flyout.prototype.dispose = function() { this.svgGroup_ = null; } this.svgBackground_ = null; - this.targetWorkspace_ = null; + this.targetWorkspace = null; }; /** @@ -367,7 +393,7 @@ Blockly.Flyout.prototype.updateDisplay_ = function() { this.svgGroup_.style.display = show ? 'block' : 'none'; // Update the scrollbar's visibility too since it should mimic the // flyout's visibility. - this.scrollbar_.setContainerVisible(show); + this.scrollbar.setContainerVisible(show); }; /** @@ -392,14 +418,14 @@ Blockly.Flyout.prototype.positionAt_ = function(width, height, x, y) { } // Update the scrollbar (if one exists). - if (this.scrollbar_) { + if (this.scrollbar) { // Set the scrollbars origin to be the top left of the flyout. - this.scrollbar_.setOrigin(x, y); - this.scrollbar_.resize(); + this.scrollbar.setOrigin(x, y); + this.scrollbar.resize(); // Set the position again so that if the metrics were the same (and the // resize failed) our position is still updated. - this.scrollbar_.setPosition_( - this.scrollbar_.position_.x, this.scrollbar_.position_.y); + this.scrollbar.setPosition( + this.scrollbar.position.x, this.scrollbar.position.y); } }; @@ -426,82 +452,41 @@ Blockly.Flyout.prototype.hide = function() { /** * Show and populate the flyout. - * @param {!Array|!NodeList|string} xmlList List of blocks to show. - * Variables and procedures have a custom set of blocks. + * @param {!Blockly.utils.toolbox.ToolboxDefinition|string} flyoutDef + * List of contents to display in the flyout as an array of xml an + * array of Nodes, a NodeList or a string with the name of the dynamic category. + * Variables and procedures have a custom set of blocks. */ -Blockly.Flyout.prototype.show = function(xmlList) { +Blockly.Flyout.prototype.show = function(flyoutDef) { this.workspace_.setResizesEnabled(false); this.hide(); this.clearOldBlocks_(); - // Handle dynamic categories, represented by a name instead of a list of XML. + // Handle dynamic categories, represented by a name instead of a list. // Look up the correct category generation function and call that to get a // valid XML list. - if (typeof xmlList == 'string') { + if (typeof flyoutDef == 'string') { var fnToApply = this.workspace_.targetWorkspace.getToolboxCategoryCallback( - xmlList); + flyoutDef); if (typeof fnToApply != 'function') { throw TypeError('Couldn\'t find a callback function when opening' + ' a toolbox category.'); } - xmlList = fnToApply(this.workspace_.targetWorkspace); - if (!Array.isArray(xmlList)) { + flyoutDef = fnToApply(this.workspace_.targetWorkspace); + if (!Array.isArray(flyoutDef)) { throw TypeError('Result of toolbox category callback must be an array.'); } } - this.setVisible(true); - // Create the blocks to be shown in this flyout. - var contents = []; - var gaps = []; - this.permanentlyDisabled_.length = 0; - var default_gap = this.horizontalLayout_ ? this.GAP_X : this.GAP_Y; - for (var i = 0, xml; (xml = xmlList[i]); i++) { - if (!xml.tagName) { - continue; - } - switch (xml.tagName.toUpperCase()) { - case 'BLOCK': - var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); - if (!curBlock.isEnabled()) { - // Record blocks that were initially disabled. - // Do not enable these blocks as a result of capacity filtering. - this.permanentlyDisabled_.push(curBlock); - } - contents.push({type: 'block', block: curBlock}); - // This is a deprecated method for adding gap to a block. - // - var gap = parseInt(xml.getAttribute('gap'), 10); - gaps.push(isNaN(gap) ? default_gap : gap); - break; - case 'SEP': - // Change the gap between two toolbox elements. - // - // The default gap is 24, can be set larger or smaller. - // This overwrites the gap attribute on the previous element. - var newGap = parseInt(xml.getAttribute('gap'), 10); - // Ignore gaps before the first block. - if (!isNaN(newGap) && gaps.length > 0) { - gaps[gaps.length - 1] = newGap; - } else { - gaps.push(default_gap); - } - break; - case 'LABEL': - case 'BUTTON': - var isLabel = xml.tagName.toUpperCase() == 'LABEL'; - if (!Blockly.FlyoutButton) { - throw Error('Missing require for Blockly.FlyoutButton'); - } - var curButton = new Blockly.FlyoutButton(this.workspace_, - this.targetWorkspace_, xml, isLabel); - contents.push({type: 'button', button: curButton}); - gaps.push(default_gap); - break; - } - } + + // Parse the Array or NodeList passed in into an Array of + // Blockly.utils.toolbox.Toolbox. + var parsedContent = Blockly.utils.toolbox.convertToolboxToJSON(flyoutDef); + var flyoutInfo = + /** @type {{contents:!Array., gaps:!Array.}} */ ( + this.createFlyoutInfo_(parsedContent)); - this.layout_(contents, gaps); + this.layout_(flyoutInfo.contents, flyoutInfo.gaps); // IE 11 is an incompetent browser that fails to fire mouseout events. // When the mouse is over the background, deselect all blocks. @@ -515,7 +500,7 @@ Blockly.Flyout.prototype.show = function(xmlList) { this.listeners_.push(Blockly.bindEventWithChecks_(this.svgBackground_, 'mouseover', this, deselectAll)); - if (this.horizontalLayout_) { + if (this.horizontalLayout) { this.height_ = 0; } else { this.width_ = 0; @@ -532,6 +517,142 @@ Blockly.Flyout.prototype.show = function(xmlList) { this.workspace_.addChangeListener(this.reflowWrapper_); }; +/** + * Create the contents array and gaps array necessary to create the layout for + * the flyout. + * @param {Array.} parsedContent The array + * of objects to show in the flyout. + * @return {{contents:Array., gaps:Array.}} The list of contents + * and gaps needed to lay out the flyout. + * @private + */ +Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) { + var contents = []; + var gaps = []; + this.permanentlyDisabled_.length = 0; + var defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y; + for (var i = 0, contentInfo; (contentInfo = parsedContent[i]); i++) { + switch (contentInfo['kind'].toUpperCase()) { + case 'BLOCK': + var blockInfo = /** @type {Blockly.utils.toolbox.Block} */ (contentInfo); + var blockXml = this.getBlockXml_(blockInfo); + var block = this.createBlock_(blockXml); + // This is a deprecated method for adding gap to a block. + // + var gap = parseInt(blockInfo['gap'] || blockXml.getAttribute('gap'), 10); + gaps.push(isNaN(gap) ? defaultGap : gap); + contents.push({type: 'block', block: block}); + break; + case 'SEP': + var sepInfo = /** @type {Blockly.utils.toolbox.Separator} */ (contentInfo); + this.addSeparatorGap_(sepInfo, gaps, defaultGap); + break; + case 'LABEL': + var labelInfo = /** @type {Blockly.utils.toolbox.Label} */ (contentInfo); + // A label is a button with different styling. + // Rename this function. + var label = this.createButton_(labelInfo, /** isLabel */ true); + contents.push({type: 'button', button: label}); + gaps.push(defaultGap); + break; + case 'BUTTON': + var buttonInfo = /** @type {Blockly.utils.toolbox.Button} */ (contentInfo); + var button = this.createButton_(buttonInfo, /** isLabel */ false); + contents.push({type: 'button', button: button}); + gaps.push(defaultGap); + break; + } + } + return {contents: contents, gaps: gaps}; +}; + +/** + * Creates a flyout button or a flyout label. + * @param {!Blockly.utils.toolbox.Button|!Blockly.utils.toolbox.Label} btnInfo + * The object holding information about a button or a label. + * @param {boolean} isLabel True if the button is a label, false otherwise. + * @return {!Blockly.FlyoutButton} The object used to display the button in the + * flyout. + * @private + */ +Blockly.Flyout.prototype.createButton_ = function(btnInfo, isLabel) { + if (!Blockly.FlyoutButton) { + throw Error('Missing require for Blockly.FlyoutButton'); + } + var curButton = new Blockly.FlyoutButton(this.workspace_, + /** @type {!Blockly.WorkspaceSvg} */ (this.targetWorkspace), btnInfo, + isLabel); + return curButton; +}; + +/** + * Create a block from the xml and permanently disable any blocks that were + * defined as disabled. + * @param {!Element} blockXml The xml of the block. + * @return {!Blockly.BlockSvg} The block created from the blockXml. + * @private + */ +Blockly.Flyout.prototype.createBlock_ = function(blockXml) { + var curBlock = /** @type {!Blockly.BlockSvg} */ ( + Blockly.Xml.domToBlock(blockXml, this.workspace_)); + if (!curBlock.isEnabled()) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabled_.push(curBlock); + } + return curBlock; +}; + +/** + * Get the xml from the block info object. + * @param {!Blockly.utils.toolbox.Block} blockInfo The object holding + * information about a block. + * @return {!Element} The xml for the block. + * @throws {Error} if the xml is not a valid block definition. + * @private + */ +Blockly.Flyout.prototype.getBlockXml_ = function(blockInfo) { + var blockElement = null; + var blockXml = blockInfo['blockxml']; + + if (blockXml && typeof blockXml != 'string') { + blockElement = blockXml; + } else if (blockXml && typeof blockXml == 'string') { + blockElement = Blockly.Xml.textToDom(blockXml); + } else if (blockInfo['type']) { + blockElement = Blockly.utils.xml.createElement('xml'); + blockElement.setAttribute('type', blockInfo['type']); + blockElement.setAttribute('disabled', blockInfo['disabled']); + } + + if (!blockElement) { + throw Error('Error: Invalid block definition. Block definition must have blockxml or type.'); + } + return blockElement; +}; + +/** + * Add the necessary gap in the flyout for a separator. + * @param {!Blockly.utils.toolbox.Separator} sepInfo The object holding + * information about a separator. + * @param {!Array.} gaps The list gaps between items in the flyout. + * @param {number} defaultGap The default gap between the button and next element. + * @private + */ +Blockly.Flyout.prototype.addSeparatorGap_ = function(sepInfo, gaps, defaultGap) { + // Change the gap between two toolbox elements. + // + // The default gap is 24, can be set larger or smaller. + // This overwrites the gap attribute on the previous element. + var newGap = parseInt(sepInfo['gap'], 10); + // Ignore gaps before the first block. + if (!isNaN(newGap) && gaps.length > 0) { + gaps[gaps.length - 1] = newGap; + } else { + gaps.push(defaultGap); + } +}; + /** * Delete blocks, mats and buttons from a previous showing of the flyout. * @private @@ -566,7 +687,7 @@ Blockly.Flyout.prototype.clearOldBlocks_ = function() { /** * Add listeners to a block that has been added to the flyout. * @param {!SVGElement} root The root node of the SVG group the block is in. - * @param {!Blockly.Block} block The block to add listeners for. + * @param {!Blockly.BlockSvg} block The block to add listeners for. * @param {!SVGElement} rect The invisible rectangle under the block that acts * as a mat for that block. * @protected @@ -588,14 +709,14 @@ Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) { /** * Handle a mouse-down on an SVG block in a non-closing flyout. - * @param {!Blockly.Block} block The flyout block to copy. + * @param {!Blockly.BlockSvg} block The flyout block to copy. * @return {!Function} Function to call when block is clicked. * @private */ Blockly.Flyout.prototype.blockMouseDown_ = function(block) { var flyout = this; return function(e) { - var gesture = flyout.targetWorkspace_.getGesture(e); + var gesture = flyout.targetWorkspace.getGesture(e); if (gesture) { gesture.setStartBlock(block); gesture.handleFlyoutStart(e, flyout); @@ -609,7 +730,7 @@ Blockly.Flyout.prototype.blockMouseDown_ = function(block) { * @private */ Blockly.Flyout.prototype.onMouseDown_ = function(e) { - var gesture = this.targetWorkspace_.getGesture(e); + var gesture = this.targetWorkspace.getGesture(e); if (gesture) { gesture.handleFlyoutStart(e, this); } @@ -637,8 +758,8 @@ Blockly.Flyout.prototype.isBlockCreatable_ = function(block) { Blockly.Flyout.prototype.createBlock = function(originalBlock) { var newBlock = null; Blockly.Events.disable(); - var variablesBeforeCreation = this.targetWorkspace_.getAllVariables(); - this.targetWorkspace_.setResizesEnabled(false); + var variablesBeforeCreation = this.targetWorkspace.getAllVariables(); + this.targetWorkspace.setResizesEnabled(false); try { newBlock = this.placeNewBlock_(originalBlock); // Close the flyout. @@ -647,7 +768,7 @@ Blockly.Flyout.prototype.createBlock = function(originalBlock) { Blockly.Events.enable(); } - var newVariables = Blockly.Variables.getAddedVariables(this.targetWorkspace_, + var newVariables = Blockly.Variables.getAddedVariables(this.targetWorkspace, variablesBeforeCreation); if (Blockly.Events.isEnabled()) { @@ -690,7 +811,7 @@ Blockly.Flyout.prototype.initFlyoutButton_ = function(button, x, y) { /** * Create and place a rectangle corresponding to the given block. - * @param {!Blockly.Block} block The block to associate the rect to. + * @param {!Blockly.BlockSvg} block The block to associate the rect to. * @param {number} x The x position of the cursor during this layout pass. * @param {number} y The y position of the cursor during this layout pass. * @param {!{height: number, width: number}} blockHW The height and width of the @@ -749,7 +870,7 @@ Blockly.Flyout.prototype.filterForCapacity_ = function() { var blocks = this.workspace_.getTopBlocks(false); for (var i = 0, block; (block = blocks[i]); i++) { if (this.permanentlyDisabled_.indexOf(block) == -1) { - var enable = this.targetWorkspace_ + var enable = this.targetWorkspace .isCapacityAvailable(Blockly.utils.getBlockTypeCounts(block)); while (block) { block.setEnabled(enable); @@ -778,7 +899,7 @@ Blockly.Flyout.prototype.reflow = function() { * @package */ Blockly.Flyout.prototype.isScrollable = function() { - return this.scrollbar_ ? this.scrollbar_.isVisible() : false; + return this.scrollbar ? this.scrollbar.isVisible() : false; }; /** @@ -788,7 +909,7 @@ Blockly.Flyout.prototype.isScrollable = function() { * @private */ Blockly.Flyout.prototype.placeNewBlock_ = function(oldBlock) { - var targetWorkspace = this.targetWorkspace_; + var targetWorkspace = this.targetWorkspace; var svgRootOld = oldBlock.getSvgRoot(); if (!svgRootOld) { throw Error('oldBlock is not rendered.'); @@ -850,3 +971,67 @@ Blockly.Flyout.prototype.onBlocklyAction = function(action) { var cursor = this.workspace_.getCursor(); return cursor.onBlocklyAction(action); }; + +/** + * Return the deletion rectangle for this flyout in viewport coordinates. + * @return {Blockly.utils.Rect} Rectangle in which to delete. + */ +Blockly.Flyout.prototype.getClientRect; + +/** + * Position the flyout. + * @return {void} + */ +Blockly.Flyout.prototype.position; + +/** + * Determine if a drag delta is toward the workspace, based on the position + * and orientation of the flyout. This is used in determineDragIntention_ to + * determine if a new block should be created or if the flyout should scroll. + * @param {!Blockly.utils.Coordinate} currentDragDeltaXY How far the pointer has + * moved from the position at mouse down, in pixel units. + * @return {boolean} True if the drag is toward the workspace. + * @package + */ +Blockly.Flyout.prototype.isDragTowardWorkspace; + +/** + * Return an object with all the metrics required to size scrollbars for the + * flyout. + * @return {Blockly.utils.Metrics} Contains size and position metrics of the + * flyout. + * @protected + */ +Blockly.Flyout.prototype.getMetrics_; + +/** + * Sets the translation of the flyout to match the scrollbars. + * @param {!{x:number,y:number}} xyRatio Contains a y property which is a float + * between 0 and 1 specifying the degree of scrolling and a + * similar x property. + * @protected + */ +Blockly.Flyout.prototype.setMetrics_; + +/** + * Lay out the blocks in the flyout. + * @param {!Array.} contents The blocks and buttons to lay out. + * @param {!Array.} gaps The visible gaps between blocks. + * @protected + */ +Blockly.Flyout.prototype.layout_; + +/** + * Scroll the flyout. + * @param {!Event} e Mouse wheel scroll event. + * @protected + */ +Blockly.Flyout.prototype.wheel_; + +/** + * Compute height of flyout. Position mat under each block. + * For RTL: Lay out the blocks right-aligned. + * @return {void} + * @protected + */ +Blockly.Flyout.prototype.reflowInternal_; diff --git a/core/flyout_button.js b/core/flyout_button.js index f4cead8af..8585c7187 100644 --- a/core/flyout_button.js +++ b/core/flyout_button.js @@ -23,11 +23,13 @@ goog.require('Blockly.utils.dom'); * @param {!Blockly.WorkspaceSvg} workspace The workspace in which to place this * button. * @param {!Blockly.WorkspaceSvg} targetWorkspace The flyout's target workspace. - * @param {!Element} xml The XML specifying the label/button. + * @param {!Blockly.utils.toolbox.Button|!Blockly.utils.toolbox.Label} json + * The JSON specifying the label/button. * @param {boolean} isLabel Whether this button should be styled as a label. * @constructor + * @package */ -Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) { +Blockly.FlyoutButton = function(workspace, targetWorkspace, json, isLabel) { // Labels behave the same as buttons, but are styled differently. /** @@ -46,7 +48,7 @@ Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) { * @type {string} * @private */ - this.text_ = xml.getAttribute('text'); + this.text_ = json['text']; /** * @type {!Blockly.utils.Coordinate} @@ -66,16 +68,16 @@ Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) { * @type {string} * @private */ - this.callbackKey_ = xml.getAttribute('callbackKey') || + this.callbackKey_ = json['callbackKey'] || /* Check the lower case version too to satisfy IE */ - xml.getAttribute('callbackkey'); + json['callbackkey']; /** * If specified, a CSS class to add to this button. * @type {?string} * @private */ - this.cssClass_ = xml.getAttribute('web-class') || null; + this.cssClass_ = json['web-class'] || null; /** * Mouse up event data. @@ -166,7 +168,7 @@ Blockly.FlyoutButton.prototype.createDom = function() { var fontMetrics = Blockly.utils.dom.measureFontMetrics(text, fontSize, fontWeight, fontFamily); this.height = fontMetrics.height; - + if (!this.isLabel_) { this.width += 2 * Blockly.FlyoutButton.MARGIN_X; this.height += 2 * Blockly.FlyoutButton.MARGIN_Y; diff --git a/core/flyout_dragger.js b/core/flyout_dragger.js index 3b0828e8c..f49243b7f 100644 --- a/core/flyout_dragger.js +++ b/core/flyout_dragger.js @@ -37,7 +37,7 @@ Blockly.FlyoutDragger = function(flyout) { * @type {!Blockly.Scrollbar} * @private */ - this.scrollbar_ = flyout.scrollbar_; + this.scrollbar_ = flyout.scrollbar; /** * Whether the flyout scrolls horizontally. If false, the flyout scrolls @@ -45,7 +45,7 @@ Blockly.FlyoutDragger = function(flyout) { * @type {boolean} * @private */ - this.horizontalLayout_ = flyout.horizontalLayout_; + this.horizontalLayout_ = flyout.horizontalLayout; }; Blockly.utils.object.inherits(Blockly.FlyoutDragger, Blockly.WorkspaceDragger); diff --git a/core/flyout_horizontal.js b/core/flyout_horizontal.js index 7efea87f5..7f648ec65 100644 --- a/core/flyout_horizontal.js +++ b/core/flyout_horizontal.js @@ -20,6 +20,8 @@ goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Rect'); goog.require('Blockly.WidgetDiv'); +goog.requireType('Blockly.utils.Metrics'); + /** * Class for a flyout. @@ -29,17 +31,9 @@ goog.require('Blockly.WidgetDiv'); * @constructor */ Blockly.HorizontalFlyout = function(workspaceOptions) { - workspaceOptions.getMetrics = /** @type {function():!Object} */ ( - this.getMetrics_.bind(this)); - workspaceOptions.setMetrics = this.setMetrics_.bind(this); - Blockly.HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions); - /** - * Flyout should be laid out horizontally. - * @type {boolean} - * @private - */ - this.horizontalLayout_ = true; + + this.horizontalLayout = true; }; Blockly.utils.object.inherits(Blockly.HorizontalFlyout, Blockly.Flyout); @@ -56,8 +50,9 @@ Blockly.utils.object.inherits(Blockly.HorizontalFlyout, Blockly.Flyout); * .viewLeft: Offset of the left edge of visible rectangle from parent, * .contentLeft: Offset of the left-most content from the x=0 coordinate, * .absoluteLeft: Left-edge of view. - * @return {Object} Contains size and position metrics of the flyout. - * @private + * @return {Blockly.utils.Metrics} Contains size and position metrics of the + * flyout. + * @protected */ Blockly.HorizontalFlyout.prototype.getMetrics_ = function() { if (!this.isVisible()) { @@ -84,14 +79,16 @@ Blockly.HorizontalFlyout.prototype.getMetrics_ = function() { var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING; var metrics = { - viewHeight: viewHeight, - viewWidth: viewWidth, contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale, contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale, - viewTop: -this.workspace_.scrollY, - viewLeft: -this.workspace_.scrollX, contentTop: 0, contentLeft: 0, + + viewHeight: viewHeight, + viewWidth: viewWidth, + viewTop: -this.workspace_.scrollY, + viewLeft: -this.workspace_.scrollX, + absoluteTop: absoluteTop, absoluteLeft: absoluteLeft }; @@ -100,10 +97,10 @@ Blockly.HorizontalFlyout.prototype.getMetrics_ = function() { /** * Sets the translation of the flyout to match the scrollbars. - * @param {!Object} xyRatio Contains a y property which is a float + * @param {!{x:number,y:number}} xyRatio Contains a y property which is a float * between 0 and 1 specifying the degree of scrolling and a * similar x property. - * @private + * @protected */ Blockly.HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) { var metrics = this.getMetrics_(); @@ -127,7 +124,7 @@ Blockly.HorizontalFlyout.prototype.position = function() { if (!this.isVisible()) { return; } - var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics(); + var targetWorkspaceMetrics = this.targetWorkspace.getMetrics(); if (!targetWorkspaceMetrics) { // Hidden components will return null. return; @@ -142,7 +139,7 @@ Blockly.HorizontalFlyout.prototype.position = function() { // X is always 0 since this is a horizontal flyout. var x = 0; // If this flyout is the toolbox flyout. - if (this.targetWorkspace_.toolboxPosition == this.toolboxPosition_) { + if (this.targetWorkspace.toolboxPosition == this.toolboxPosition_) { // If there is a toolbox. if (targetWorkspaceMetrics.toolboxHeight) { if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) { @@ -220,13 +217,13 @@ Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, * Scroll the flyout to the top. */ Blockly.HorizontalFlyout.prototype.scrollToStart = function() { - this.scrollbar_.set(this.RTL ? Infinity : 0); + this.scrollbar.set(this.RTL ? Infinity : 0); }; /** * Scroll the flyout. * @param {!Event} e Mouse wheel scroll event. - * @private + * @protected */ Blockly.HorizontalFlyout.prototype.wheel_ = function(e) { var scrollDelta = Blockly.utils.getScrollDeltaPixels(e); @@ -238,9 +235,10 @@ Blockly.HorizontalFlyout.prototype.wheel_ = function(e) { var limit = metrics.contentWidth - metrics.viewWidth; pos = Math.min(pos, limit); pos = Math.max(pos, 0); - this.scrollbar_.set(pos); - // When the flyout moves from a wheel event, hide WidgetDiv. + this.scrollbar.set(pos); + // When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv. Blockly.WidgetDiv.hide(); + Blockly.DropDownDiv.hideWithoutAnimation(); } // Don't scroll the page. @@ -253,10 +251,10 @@ Blockly.HorizontalFlyout.prototype.wheel_ = function(e) { * Lay out the blocks in the flyout. * @param {!Array.} contents The blocks and buttons to lay out. * @param {!Array.} gaps The visible gaps between blocks. - * @private + * @protected */ Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) { - this.workspace_.scale = this.targetWorkspace_.scale; + this.workspace_.scale = this.targetWorkspace.scale; var margin = this.MARGIN; var cursorX = margin + this.tabWidth_; var cursorY = margin; @@ -350,10 +348,10 @@ Blockly.HorizontalFlyout.prototype.getClientRect = function() { /** * Compute height of flyout. Position mat under each block. * For RTL: Lay out the blocks right-aligned. - * @private + * @protected */ Blockly.HorizontalFlyout.prototype.reflowInternal_ = function() { - this.workspace_.scale = this.targetWorkspace_.scale; + this.workspace_.scale = this.targetWorkspace.scale; var flyoutHeight = 0; var blocks = this.workspace_.getTopBlocks(false); for (var i = 0, block; (block = blocks[i]); i++) { diff --git a/core/flyout_vertical.js b/core/flyout_vertical.js index ae27031ee..139277cc2 100644 --- a/core/flyout_vertical.js +++ b/core/flyout_vertical.js @@ -21,6 +21,8 @@ goog.require('Blockly.utils.Rect'); goog.require('Blockly.utils.userAgent'); goog.require('Blockly.WidgetDiv'); +goog.requireType('Blockly.utils.Metrics'); + /** * Class for a flyout. @@ -30,17 +32,7 @@ goog.require('Blockly.WidgetDiv'); * @constructor */ Blockly.VerticalFlyout = function(workspaceOptions) { - workspaceOptions.getMetrics = /** @type {function():!Object} */ ( - this.getMetrics_.bind(this)); - workspaceOptions.setMetrics = this.setMetrics_.bind(this); - Blockly.VerticalFlyout.superClass_.constructor.call(this, workspaceOptions); - /** - * Flyout should be laid out vertically. - * @type {boolean} - * @private - */ - this.horizontalLayout_ = false; }; Blockly.utils.object.inherits(Blockly.VerticalFlyout, Blockly.Flyout); @@ -57,8 +49,9 @@ Blockly.utils.object.inherits(Blockly.VerticalFlyout, Blockly.Flyout); * .viewLeft: Offset of the left edge of visible rectangle from parent, * .contentLeft: Offset of the left-most content from the x=0 coordinate, * .absoluteLeft: Left-edge of view. - * @return {Object} Contains size and position metrics of the flyout. - * @private + * @return {Blockly.utils.Metrics} Contains size and position metrics of the + * flyout. + * @protected */ Blockly.VerticalFlyout.prototype.getMetrics_ = function() { if (!this.isVisible()) { @@ -84,14 +77,16 @@ Blockly.VerticalFlyout.prototype.getMetrics_ = function() { } var metrics = { - viewHeight: viewHeight, - viewWidth: viewWidth, contentHeight: optionBox.height * this.workspace_.scale + 2 * this.MARGIN, contentWidth: optionBox.width * this.workspace_.scale + 2 * this.MARGIN, - viewTop: -this.workspace_.scrollY + optionBox.y, - viewLeft: -this.workspace_.scrollX, contentTop: optionBox.y, contentLeft: optionBox.x, + + viewHeight: viewHeight, + viewWidth: viewWidth, + viewTop: -this.workspace_.scrollY + optionBox.y, + viewLeft: -this.workspace_.scrollX, + absoluteTop: absoluteTop, absoluteLeft: absoluteLeft }; @@ -100,10 +95,10 @@ Blockly.VerticalFlyout.prototype.getMetrics_ = function() { /** * Sets the translation of the flyout to match the scrollbars. - * @param {!Object} xyRatio Contains a y property which is a float + * @param {!{x:number,y:number}} xyRatio Contains a y property which is a float * between 0 and 1 specifying the degree of scrolling and a * similar x property. - * @private + * @protected */ Blockly.VerticalFlyout.prototype.setMetrics_ = function(xyRatio) { var metrics = this.getMetrics_(); @@ -125,7 +120,7 @@ Blockly.VerticalFlyout.prototype.position = function() { if (!this.isVisible()) { return; } - var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics(); + var targetWorkspaceMetrics = this.targetWorkspace.getMetrics(); if (!targetWorkspaceMetrics) { // Hidden components will return null. return; @@ -140,7 +135,7 @@ Blockly.VerticalFlyout.prototype.position = function() { // Y is always 0 since this is a vertical flyout. var y = 0; // If this flyout is the toolbox flyout. - if (this.targetWorkspace_.toolboxPosition == this.toolboxPosition_) { + if (this.targetWorkspace.toolboxPosition == this.toolboxPosition_) { // If there is a category toolbox. if (targetWorkspaceMetrics.toolboxWidth) { if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) { @@ -208,13 +203,13 @@ Blockly.VerticalFlyout.prototype.setBackgroundPath_ = function(width, height) { * Scroll the flyout to the top. */ Blockly.VerticalFlyout.prototype.scrollToStart = function() { - this.scrollbar_.set(0); + this.scrollbar.set(0); }; /** * Scroll the flyout. * @param {!Event} e Mouse wheel scroll event. - * @private + * @protected */ Blockly.VerticalFlyout.prototype.wheel_ = function(e) { var scrollDelta = Blockly.utils.getScrollDeltaPixels(e); @@ -225,9 +220,10 @@ Blockly.VerticalFlyout.prototype.wheel_ = function(e) { var limit = metrics.contentHeight - metrics.viewHeight; pos = Math.min(pos, limit); pos = Math.max(pos, 0); - this.scrollbar_.set(pos); - // When the flyout moves from a wheel event, hide WidgetDiv. + this.scrollbar.set(pos); + // When the flyout moves from a wheel event, hide WidgetDiv and DropDownDiv. Blockly.WidgetDiv.hide(); + Blockly.DropDownDiv.hideWithoutAnimation(); } // Don't scroll the page. @@ -240,10 +236,10 @@ Blockly.VerticalFlyout.prototype.wheel_ = function(e) { * Lay out the blocks in the flyout. * @param {!Array.} contents The blocks and buttons to lay out. * @param {!Array.} gaps The visible gaps between blocks. - * @private + * @protected */ Blockly.VerticalFlyout.prototype.layout_ = function(contents, gaps) { - this.workspace_.scale = this.targetWorkspace_.scale; + this.workspace_.scale = this.targetWorkspace.scale; var margin = this.MARGIN; var cursorX = this.RTL ? margin : margin + this.tabWidth_; var cursorY = margin; @@ -329,10 +325,10 @@ Blockly.VerticalFlyout.prototype.getClientRect = function() { /** * Compute width of flyout. Position mat under each block. * For RTL: Lay out the blocks and buttons to be right-aligned. - * @private + * @protected */ Blockly.VerticalFlyout.prototype.reflowInternal_ = function() { - this.workspace_.scale = this.targetWorkspace_.scale; + this.workspace_.scale = this.targetWorkspace.scale; var flyoutWidth = 0; var blocks = this.workspace_.getTopBlocks(false); for (var i = 0, block; (block = blocks[i]); i++) { diff --git a/core/generator.js b/core/generator.js index 6937042d5..f556f6894 100644 --- a/core/generator.js +++ b/core/generator.js @@ -172,6 +172,10 @@ Blockly.Generator.prototype.blockToCode = function(block, opt_thisOnly) { // Skip past this block if it is disabled. return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock()); } + if (block.isInsertionMarker()) { + // Skip past insertion markers. + return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]); + } var func = this[block.type]; if (typeof func != 'function') { diff --git a/core/gesture.js b/core/gesture.js index 3de14fc38..c100f95f5 100644 --- a/core/gesture.js +++ b/core/gesture.js @@ -314,7 +314,7 @@ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { } if (!this.flyout_.isScrollable() || this.flyout_.isDragTowardWorkspace(this.currentDragDeltaXY_)) { - this.startWorkspace_ = this.flyout_.targetWorkspace_; + this.startWorkspace_ = this.flyout_.targetWorkspace; this.startWorkspace_.updateScreenCalculationsIfScrolled(); // Start the event group now, so that the same event group is used for block // creation and block dragging. @@ -489,7 +489,7 @@ Blockly.Gesture.prototype.doStart = function(e) { e.shiftKey && this.targetBlock_.workspace.keyboardAccessibilityMode) { this.creatorWorkspace_.getCursor().setCurNode( - Blockly.navigation.getTopNode(this.targetBlock_)); + Blockly.ASTNode.createTopNode(this.targetBlock_)); } else { this.targetBlock_.select(); } @@ -656,6 +656,17 @@ Blockly.Gesture.prototype.handleWsStart = function(e, ws) { } }; +/** + * Fires a workspace click event. + * @param {!Blockly.WorkspaceSvg} ws The workspace that a user clicks on. + * @private + */ +Blockly.Gesture.prototype.fireWorkspaceClick_ = function(ws) { + var clickEvent = new Blockly.Events.Ui(null, 'workspaceClick', null, null); + clickEvent.workspaceId = ws.id; + Blockly.Events.fire(clickEvent); +}; + /** * Handle a mousedown/touchstart event on a flyout. * @param {!Event} e A mouse down or touch start event. @@ -763,6 +774,7 @@ Blockly.Gesture.prototype.doWorkspaceClick_ = function(e) { } else if (Blockly.selected) { Blockly.selected.unselect(); } + this.fireWorkspaceClick_(ws); }; /* End functions defining what actions to take to execute clicks on each type diff --git a/core/icon.js b/core/icon.js index 7123dbc6a..0a180ce69 100644 --- a/core/icon.js +++ b/core/icon.js @@ -22,6 +22,7 @@ goog.require('Blockly.utils.Size'); * Class for an icon. * @param {Blockly.BlockSvg} block The block associated with this icon. * @constructor + * @abstract */ Blockly.Icon = function(block) { /** @@ -181,3 +182,10 @@ Blockly.Icon.prototype.getCorrectedSize = function() { return new Blockly.utils.Size( Blockly.Icon.prototype.SIZE, Blockly.Icon.prototype.SIZE - 2); }; + +/** + * Draw the icon. + * @param {!Element} group The icon group. + * @protected + */ +Blockly.Icon.prototype.drawIcon_; diff --git a/core/inject.js b/core/inject.js index cc599ab9f..93651f834 100644 --- a/core/inject.js +++ b/core/inject.js @@ -29,6 +29,8 @@ goog.require('Blockly.utils.userAgent'); goog.require('Blockly.WorkspaceDragSurfaceSvg'); goog.require('Blockly.WorkspaceSvg'); +goog.requireType('Blockly.utils.Metrics'); + /** * Inject a Blockly editor into the specified container element (usually a div). @@ -364,7 +366,7 @@ Blockly.init_ = function(mainWorkspace) { } else if (flyout) { // Build a fixed flyout with the root blocks. flyout.init(mainWorkspace); - flyout.show(options.languageTree.childNodes); + flyout.show(options.languageTree); flyout.scrollToStart(); } } diff --git a/core/input.js b/core/input.js index 8e6bfb7b4..ab6db5303 100644 --- a/core/input.js +++ b/core/input.js @@ -91,35 +91,39 @@ Blockly.Input.prototype.insertFieldAt = function(index, field, opt_name) { if (index < 0 || index > this.fieldRow.length) { throw Error('index ' + index + ' out of bounds.'); } - // Falsy field values don't generate a field, unless the field is an empty // string and named. if (!field && !(field == '' && opt_name)) { return index; } + // Generate a FieldLabel when given a plain text field. if (typeof field == 'string') { field = new Blockly.FieldLabel(/** @type {string} */ (field)); } + field.setSourceBlock(this.sourceBlock_); if (this.sourceBlock_.rendered) { field.init(); } field.name = opt_name; + field.setVisible(this.isVisible()); - if (field.prefixField) { + var fieldDropdown = /** @type {Blockly.FieldDropdown} */ (field); + if (fieldDropdown.prefixField) { // Add any prefix. - index = this.insertFieldAt(index, field.prefixField); + index = this.insertFieldAt(index, fieldDropdown.prefixField); } // Add the field to the field row. this.fieldRow.splice(index, 0, field); ++index; - if (field.suffixField) { + if (fieldDropdown.suffixField) { // Add any suffix. - index = this.insertFieldAt(index, field.suffixField); + index = this.insertFieldAt(index, fieldDropdown.suffixField); } if (this.sourceBlock_.rendered) { + this.sourceBlock_ = /** @type {!Blockly.BlockSvg} */ (this.sourceBlock_); this.sourceBlock_.render(); // Adding a field will cause the block to change shape. this.sourceBlock_.bumpNeighbours(); @@ -130,22 +134,30 @@ Blockly.Input.prototype.insertFieldAt = function(index, field, opt_name) { /** * Remove a field from this input. * @param {string} name The name of the field. - * @throws {Error} if the field is not present. + * @param {boolean=} opt_quiet True to prevent an error if field is not present. + * @return {boolean} True if operation succeeds, false if field is not present + * and opt_quiet is true. + * @throws {Error} if the field is not present and opt_quiet is false. */ -Blockly.Input.prototype.removeField = function(name) { +Blockly.Input.prototype.removeField = function(name, opt_quiet) { for (var i = 0, field; (field = this.fieldRow[i]); i++) { if (field.name === name) { field.dispose(); this.fieldRow.splice(i, 1); if (this.sourceBlock_.rendered) { + this.sourceBlock_ = /** @type {!Blockly.BlockSvg} */ (this.sourceBlock_); this.sourceBlock_.render(); // Removing a field will cause the block to change shape. this.sourceBlock_.bumpNeighbours(); } - return; + return true; } } - throw Error('Field "%s" not found.', name); + if (opt_quiet) { + return false; + } else { + throw Error('Field "' + name + '" not found.'); + } }; /** @@ -160,7 +172,7 @@ Blockly.Input.prototype.isVisible = function() { * Sets whether this input is visible or not. * Should only be used to collapse/uncollapse a block. * @param {boolean} visible True if visible. - * @return {!Array.} List of blocks to render. + * @return {!Array.} List of blocks to render. * @package */ Blockly.Input.prototype.setVisible = function(visible) { @@ -173,11 +185,12 @@ Blockly.Input.prototype.setVisible = function(visible) { } this.visible_ = visible; - var display = visible ? 'block' : 'none'; for (var y = 0, field; (field = this.fieldRow[y]); y++) { field.setVisible(visible); } if (this.connection) { + this.connection = + /** @type {!Blockly.RenderedConnection} */ (this.connection); // Has a connection. if (visible) { renderList = this.connection.startTrackingAll(); @@ -186,10 +199,7 @@ Blockly.Input.prototype.setVisible = function(visible) { } var child = this.connection.targetBlock(); if (child) { - child.getSvgRoot().style.display = display; - if (!visible) { - child.rendered = false; - } + child.getSvgRoot().style.display = visible ? 'block' : 'none'; } } return renderList; @@ -228,6 +238,7 @@ Blockly.Input.prototype.setCheck = function(check) { Blockly.Input.prototype.setAlign = function(align) { this.align = align; if (this.sourceBlock_.rendered) { + this.sourceBlock_ = /** @type {!Blockly.BlockSvg} */ (this.sourceBlock_); this.sourceBlock_.render(); } return this; diff --git a/core/insertion_marker_manager.js b/core/insertion_marker_manager.js index 49f264e72..2d47dd296 100644 --- a/core/insertion_marker_manager.js +++ b/core/insertion_marker_manager.js @@ -160,6 +160,15 @@ Blockly.InsertionMarkerManager.prototype.dispose = function() { } }; +/** + * Update the available connections for the top block. These connections can + * change if a block is unplugged and the stack is healed. + * @package + */ +Blockly.InsertionMarkerManager.prototype.updateAvailableConnections = function() { + this.availableConnections_ = this.initAvailableConnections_(); +}; + /** * Return whether the block would be deleted if dropped immediately, based on * information from the most recent move event. @@ -253,23 +262,25 @@ Blockly.InsertionMarkerManager.prototype.createMarkerBlock_ = function(sourceBlo result.domToMutation(oldMutationDom); } } - result.setCollapsed(sourceBlock.isCollapsed()); - result.setInputsInline(sourceBlock.getInputsInline()); - // Copy visible field values from the other block. These values may impact - // the rendered size of the insertion marker. Note that we do not care - // about child blocks here. + // Copy field values from the other block. These values may impact the + // rendered size of the insertion marker. Note that we do not care about + // child blocks here. for (var i = 0; i < sourceBlock.inputList.length; i++) { var sourceInput = sourceBlock.inputList[i]; - if (sourceInput.isVisible()) { - var resultInput = result.inputList[i]; - for (var j = 0; j < sourceInput.fieldRow.length; j++) { - var sourceField = sourceInput.fieldRow[j]; - var resultField = resultInput.fieldRow[j]; - resultField.setValue(sourceField.getValue()); - } + if (sourceInput.name == Blockly.Block.COLLAPSED_INPUT_NAME) { + continue; // Ignore the collapsed input. + } + var resultInput = result.inputList[i]; + for (var j = 0; j < sourceInput.fieldRow.length; j++) { + var sourceField = sourceInput.fieldRow[j]; + var resultField = resultInput.fieldRow[j]; + resultField.setValue(sourceField.getValue()); } } + result.setCollapsed(sourceBlock.isCollapsed()); + result.setInputsInline(sourceBlock.getInputsInline()); + result.initSvg(); result.getSvgRoot().setAttribute('visibility', 'hidden'); } finally { diff --git a/core/interfaces/i_accessibility.js b/core/interfaces/i_accessibility.js new file mode 100644 index 000000000..c31461b6d --- /dev/null +++ b/core/interfaces/i_accessibility.js @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview AST Node and keyboard navigation interfaces. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.IASTNodeLocation'); +goog.provide('Blockly.IASTNodeLocationSvg'); +goog.provide('Blockly.IASTNodeLocationWithBlock'); +goog.provide('Blockly.IBlocklyActionable'); + +/** + * An AST node location interface. + * @interface + */ +Blockly.IASTNodeLocation = function() {}; + +/** + * An AST node location SVG interface. + * @interface + * @extends {Blockly.IASTNodeLocation} + */ +Blockly.IASTNodeLocationSvg = function() {}; + +/** + * Add the marker svg to this node's svg group. + * @param {SVGElement} markerSvg The svg root of the marker to be added to the + * svg group. + */ +Blockly.IASTNodeLocationSvg.prototype.setMarkerSvg; + +/** + * Add the cursor svg to this node's svg group. + * @param {SVGElement} cursorSvg The svg root of the cursor to be added to the + * svg group. + */ +Blockly.IASTNodeLocationSvg.prototype.setCursorSvg; + +/** + * An AST node location that has an associated block. + * @interface + * @extends {Blockly.IASTNodeLocation} + */ +Blockly.IASTNodeLocationWithBlock = function() {}; + +/** + * Get the source block associated with this node. + * @return {Blockly.Block} The source block. + */ +Blockly.IASTNodeLocationWithBlock.prototype.getSourceBlock; + + +/** + * An interface for an object that handles Blockly actions when keyboard + * navigation is enabled. + * @interface + */ +Blockly.IBlocklyActionable = function() {}; + +/** + * Handles the given action. + * @param {!Blockly.Action} action The action to be handled. + * @return {boolean} True if the action has been handled, false otherwise. + */ +Blockly.IBlocklyActionable.prototype.onBlocklyAction; diff --git a/core/interfaces/i_bounded_element.js b/core/interfaces/i_bounded_element.js new file mode 100644 index 000000000..34b72fb32 --- /dev/null +++ b/core/interfaces/i_bounded_element.js @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for a bounded element. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.IBoundedElement'); + +goog.requireType('Blockly.utils.Rect'); + + +/** + * A bounded element interface. + * @interface + */ +Blockly.IBoundedElement = function() {}; + +/** + * Returns the coordinates of a bounded element describing the dimensions of the + * element. + * Coordinate system: workspace coordinates. + * @return {!Blockly.utils.Rect} Object with coordinates of the bounded element. + */ +Blockly.IBoundedElement.prototype.getBoundingRectangle; diff --git a/core/interfaces/i_copyable.js b/core/interfaces/i_copyable.js new file mode 100644 index 000000000..a20565dc5 --- /dev/null +++ b/core/interfaces/i_copyable.js @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for an object that is copyable. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.ICopyable'); + +goog.requireType('Blockly.ISelectable'); +goog.requireType('Blockly.WorkspaceSvg'); + + +/** + * @extends {Blockly.ISelectable} + * @interface + */ +Blockly.ICopyable = function() {}; + +/** + * Encode for copying. + * @return {!Blockly.ICopyable.CopyData} Copy metadata. + */ +Blockly.ICopyable.prototype.toCopyData; + +/** + * Copy Metadata. + * @typedef {{ + * xml:!Element, + * source:Blockly.WorkspaceSvg, + * typeCounts:?Object + * }} + */ +Blockly.ICopyable.CopyData; diff --git a/core/interfaces/i_deletable.js b/core/interfaces/i_deletable.js new file mode 100644 index 000000000..8e8524863 --- /dev/null +++ b/core/interfaces/i_deletable.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for an object that is deletable. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.IDeletable'); + + +/** + * The interface for an object that can be deleted. + * @interface + */ +Blockly.IDeletable = function() {}; + +/** + * Get whether this object is deletable or not. + * @return {boolean} True if deletable. + */ +Blockly.IDeletable.prototype.isDeletable; diff --git a/core/interfaces/i_deletearea.js b/core/interfaces/i_deletearea.js new file mode 100644 index 000000000..3ca0f083d --- /dev/null +++ b/core/interfaces/i_deletearea.js @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for a component that can delete a block that is + * dropped on top of it. + * @author aschmiedt@google.com (Abby Schmiedt) + */ + +'use strict'; + +goog.provide('Blockly.IDeleteArea'); + + +/** + * Interface for a component that can delete a block that is dropped on top of it. + * @interface + */ +Blockly.IDeleteArea = function() {}; + +/** + * Return the deletion rectangle. + * @return {Blockly.utils.Rect} Rectangle in which to delete. + */ +Blockly.IDeleteArea.prototype.getClientRect; diff --git a/core/interfaces/i_movable.js b/core/interfaces/i_movable.js new file mode 100644 index 000000000..bc0c3bf9f --- /dev/null +++ b/core/interfaces/i_movable.js @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for an object that is movable. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.IMovable'); + + +/** + * The interface for an object that is movable. + * @interface + */ +Blockly.IMovable = function() {}; + +/** + * Get whether this is movable or not. + * @return {boolean} True if movable. + */ +Blockly.IMovable.prototype.isMovable; diff --git a/core/interfaces/i_registrable.js b/core/interfaces/i_registrable.js new file mode 100644 index 000000000..ed989671b --- /dev/null +++ b/core/interfaces/i_registrable.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for a Blockly component that can be registered. + * (Ex. Toolbox, Fields, Renderers) + * @author aschmiedt@google.com (Abby Schmiedt) + */ + +'use strict'; + +goog.provide('Blockly.IRegistrable'); + + +/** + * The interface for a Blockly component that can be registered. + * @interface + * */ +Blockly.IRegistrable = function() {}; diff --git a/core/interfaces/i_selectable.js b/core/interfaces/i_selectable.js new file mode 100644 index 000000000..e3d817fde --- /dev/null +++ b/core/interfaces/i_selectable.js @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for an object that is selectable. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.ISelectable'); + +goog.requireType('Blockly.IDeletable'); +goog.requireType('Blockly.IMovable'); + + +/** + * The interface for an object that is selectable. + * @extends {Blockly.IDeletable} + * @extends {Blockly.IMovable} + * @interface + */ +Blockly.ISelectable = function() {}; + +/** + * @type {string} + */ +Blockly.ISelectable.prototype.id; + +/** + * Select this. Highlight it visually. + * @return {void} + */ +Blockly.ISelectable.prototype.select; + +/** + * Unselect this. Unhighlight it visually. + * @return {void} + */ +Blockly.ISelectable.prototype.unselect; diff --git a/core/interfaces/i_styleable.js b/core/interfaces/i_styleable.js new file mode 100644 index 000000000..944ee9d65 --- /dev/null +++ b/core/interfaces/i_styleable.js @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for an object that a style can be added to. + * @author aschmiedt@google.com (Abby Schmiedt) + */ + +'use strict'; + +goog.provide('Blockly.IStyleable'); + + +/** + * Interface for an object that a style can be added to. + * @interface + */ +Blockly.IStyleable = function() {}; + +/** + * Adds a style on the toolbox. Usually used to change the cursor. + * @param {string} style The name of the class to add. + */ +Blockly.IStyleable.prototype.addStyle; + +/** + * Removes a style from the toolbox. Usually used to change the cursor. + * @param {string} style The name of the class to remove. + */ +Blockly.IStyleable.prototype.removeStyle; diff --git a/core/interfaces/i_toolbox.js b/core/interfaces/i_toolbox.js new file mode 100644 index 000000000..456829fc5 --- /dev/null +++ b/core/interfaces/i_toolbox.js @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for a toolbox. + * @author aschmiedt@google.com (Abby Schmiedt) + */ + +'use strict'; + +goog.provide('Blockly.IToolbox'); + +goog.requireType('Blockly.IRegistrable'); + + +/** + * Interface for a toolbox. + * @extends {Blockly.IRegistrable} + * @interface + */ +Blockly.IToolbox = function() {}; + +/** + * Initializes the toolbox. + * @return {void} + */ +Blockly.IToolbox.prototype.init; + +/** + * Fill the toolbox with categories and blocks. + * @param {Array.} toolboxDef Array holding objects + * containing information on the contents of the toolbox. + */ +Blockly.IToolbox.prototype.render; + +/** + * Dispose of this toolbox. + * @return {void} + */ +Blockly.IToolbox.prototype.dispose; + +/** + * Get the width of the toolbox. + * @return {number} The width of the toolbox. + */ +Blockly.IToolbox.prototype.getWidth; + +/** + * Get the height of the toolbox. + * @return {number} The width of the toolbox. + */ +Blockly.IToolbox.prototype.getHeight; + +/** + * Get the toolbox flyout. + * @return {Blockly.Flyout} The toolbox flyout. + */ +Blockly.IToolbox.prototype.getFlyout; + +/** + * Move the toolbox to the edge. + * @return {void} + */ +Blockly.IToolbox.prototype.position; + +/** + * Unhighlight any previously specified option. + * @return {void} + */ +Blockly.IToolbox.prototype.clearSelection; + +/** + * Updates the category colours and background colour of selected categories. + * @return {void} + */ +Blockly.IToolbox.prototype.refreshTheme; + +/** + * Update the flyout's contents without closing it. Should be used in response + * to a change in one of the dynamic categories, such as variables or + * procedures. + * @return {void} + */ +Blockly.IToolbox.prototype.refreshSelection; + +/** + * Toggles the visibility of the toolbox. + * @param {boolean} isVisible True if the toolbox should be visible. + */ +Blockly.IToolbox.prototype.setVisible; + +/** + * Select the first toolbox category if no category is selected. + * @return {void} + */ +Blockly.IToolbox.prototype.selectFirstCategory; diff --git a/core/keyboard_nav/ast_node.js b/core/keyboard_nav/ast_node.js index 4f111b84c..7669aa670 100644 --- a/core/keyboard_nav/ast_node.js +++ b/core/keyboard_nav/ast_node.js @@ -14,6 +14,9 @@ goog.provide('Blockly.ASTNode'); goog.require('Blockly.utils.Coordinate'); +goog.requireType('Blockly.IASTNodeLocation'); +goog.requireType('Blockly.IASTNodeLocationWithBlock'); + /** * Class for an AST node. @@ -21,9 +24,8 @@ goog.require('Blockly.utils.Coordinate'); * creating a node directly. * @param {string} type The type of the location. * Must be in Blockly.ASTNode.types. - * @param {!(Blockly.Block|Blockly.Connection|Blockly.Field|Blockly.Workspace)} - * location The position in the AST. - * @param {!Object=} opt_params Optional dictionary of options. + * @param {!Blockly.IASTNodeLocation} location The position in the AST. + * @param {!Blockly.ASTNode.Params=} opt_params Optional dictionary of options. * @constructor */ Blockly.ASTNode = function(type, location, opt_params) { @@ -48,14 +50,28 @@ Blockly.ASTNode = function(type, location, opt_params) { /** * The location of the AST node. - * @type {!(Blockly.Block|Blockly.Connection|Blockly.Field|Blockly.Workspace)} + * @type {!Blockly.IASTNodeLocation} * @private */ this.location_ = location; + /** + * The coordinate on the workspace. + * @type {Blockly.utils.Coordinate} + * @private + */ + this.wsCoordinate_ = null; + this.processParams_(opt_params || null); }; +/** + * @typedef {{ + * wsCoordinate: Blockly.utils.Coordinate + * }} + */ +Blockly.ASTNode.Params; + /** * Object holding different types for an AST node. * @enum {string} @@ -199,9 +215,27 @@ Blockly.ASTNode.createWorkspaceNode = function(workspace, wsCoordinate) { Blockly.ASTNode.types.WORKSPACE, workspace, params); }; +/** + * Creates an AST node for the top position on a block. + * This is either an output connection, previous connection, or block. + * @param {!Blockly.Block} block The block to find the top most AST node on. + * @return {Blockly.ASTNode} The AST node holding the top most position on the + * block. + */ +Blockly.ASTNode.createTopNode = function(block) { + var astNode; + var topConnection = block.previousConnection || block.outputConnection; + if (topConnection) { + astNode = Blockly.ASTNode.createConnectionNode(topConnection); + } else { + astNode = Blockly.ASTNode.createBlockNode(block); + } + return astNode; +}; + /** * Parse the optional parameters. - * @param {Object} params The user specified parameters. + * @param {?Blockly.ASTNode.Params} params The user specified parameters. * @private */ Blockly.ASTNode.prototype.processParams_ = function(params) { @@ -217,8 +251,8 @@ Blockly.ASTNode.prototype.processParams_ = function(params) { * Gets the value pointed to by this node. * It is the callers responsibility to check the node type to figure out what * type of object they get back from this. - * @return {!(Blockly.Field|Blockly.Connection|Blockly.Block|Blockly.Workspace)} - * The current field, connection, workspace, or block the cursor is on. + * @return {!Blockly.IASTNodeLocation} The current field, connection, workspace, or + * block the cursor is on. */ Blockly.ASTNode.prototype.getLocation = function() { return this.location_; @@ -261,7 +295,8 @@ Blockly.ASTNode.prototype.isConnection = function() { * @private */ Blockly.ASTNode.prototype.findNextForInput_ = function() { - var parentInput = this.location_.getParentInput(); + var location = /** @type {!Blockly.Connection} */ (this.location_); + var parentInput = location.getParentInput(); var block = parentInput.getSourceBlock(); var curIdx = block.inputList.indexOf(parentInput); for (var i = curIdx + 1, input; (input = block.inputList[i]); i++) { @@ -317,11 +352,12 @@ Blockly.ASTNode.prototype.findNextForField_ = function() { * @private */ Blockly.ASTNode.prototype.findPrevForInput_ = function() { - var location = this.location_.getParentInput(); - var block = location.getSourceBlock(); - var curIdx = block.inputList.indexOf(location); + var location = /** @type {!Blockly.Connection} */ (this.location_); + var parentInput = location.getParentInput(); + var block = parentInput.getSourceBlock(); + var curIdx = block.inputList.indexOf(parentInput); for (var i = curIdx, input; (input = block.inputList[i]); i--) { - if (input.connection && input !== location) { + if (input.connection && input !== parentInput) { return Blockly.ASTNode.createInputNode(input); } var fieldRow = input.fieldRow; @@ -376,7 +412,8 @@ Blockly.ASTNode.prototype.findPrevForField_ = function() { Blockly.ASTNode.prototype.navigateBetweenStacks_ = function(forward) { var curLocation = this.getLocation(); if (!(curLocation instanceof Blockly.Block)) { - curLocation = curLocation.getSourceBlock(); + curLocation = /** @type {!Blockly.IASTNodeLocationWithBlock} */ ( + curLocation).getSourceBlock(); } if (!curLocation || !curLocation.workspace) { return null; @@ -481,7 +518,8 @@ Blockly.ASTNode.prototype.getSourceBlock = function() { } else if (this.getType() === Blockly.ASTNode.types.WORKSPACE) { return null; } else { - return this.getLocation().getSourceBlock(); + return /** @type {Blockly.IASTNodeLocationWithBlock} */ ( + this.getLocation()).getSourceBlock(); } }; @@ -496,7 +534,8 @@ Blockly.ASTNode.prototype.next = function() { return this.navigateBetweenStacks_(true); case Blockly.ASTNode.types.OUTPUT: - return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock()); + var connection = /** @type {!Blockly.Connection} */ (this.location_); + return Blockly.ASTNode.createBlockNode(connection.getSourceBlock()); case Blockly.ASTNode.types.FIELD: return this.findNextForField_(); @@ -505,14 +544,17 @@ Blockly.ASTNode.prototype.next = function() { return this.findNextForInput_(); case Blockly.ASTNode.types.BLOCK: - var nextConnection = this.location_.nextConnection; + var block = /** @type {!Blockly.Block} */ (this.location_); + var nextConnection = block.nextConnection; return Blockly.ASTNode.createConnectionNode(nextConnection); case Blockly.ASTNode.types.PREVIOUS: - return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock()); + var connection = /** @type {!Blockly.Connection} */ (this.location_); + return Blockly.ASTNode.createBlockNode(connection.getSourceBlock()); case Blockly.ASTNode.types.NEXT: - var targetConnection = this.location_.targetConnection; + var connection = /** @type {!Blockly.Connection} */ (this.location_); + var targetConnection = connection.targetConnection; return Blockly.ASTNode.createConnectionNode(targetConnection); } @@ -528,7 +570,8 @@ Blockly.ASTNode.prototype.next = function() { Blockly.ASTNode.prototype.in = function() { switch (this.type_) { case Blockly.ASTNode.types.WORKSPACE: - var topBlocks = this.location_.getTopBlocks(true); + var workspace = /** @type {!Blockly.Workspace} */ (this.location_); + var topBlocks = workspace.getTopBlocks(true); if (topBlocks.length > 0) { return Blockly.ASTNode.createStackNode(topBlocks[0]); } @@ -543,7 +586,8 @@ Blockly.ASTNode.prototype.in = function() { return this.findFirstFieldOrInput_(block); case Blockly.ASTNode.types.INPUT: - var targetConnection = this.location_.targetConnection; + var connection = /** @type {!Blockly.Connection} */ (this.location_); + var targetConnection = connection.targetConnection; return Blockly.ASTNode.createConnectionNode(targetConnection); } @@ -571,19 +615,21 @@ Blockly.ASTNode.prototype.prev = function() { return this.findPrevForInput_(); case Blockly.ASTNode.types.BLOCK: - var block = this.location_; + var block = /** @type {!Blockly.Block} */ (this.location_); var topConnection = block.previousConnection || block.outputConnection; return Blockly.ASTNode.createConnectionNode(topConnection); case Blockly.ASTNode.types.PREVIOUS: - var targetConnection = this.location_.targetConnection; + var connection = /** @type {!Blockly.Connection} */ (this.location_); + var targetConnection = connection.targetConnection; if (targetConnection && !targetConnection.getParentInput()) { return Blockly.ASTNode.createConnectionNode(targetConnection); } break; case Blockly.ASTNode.types.NEXT: - return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock()); + var connection = /** @type {!Blockly.Connection} */ (this.location_); + return Blockly.ASTNode.createBlockNode(connection.getSourceBlock()); } return null; @@ -598,35 +644,40 @@ Blockly.ASTNode.prototype.prev = function() { Blockly.ASTNode.prototype.out = function() { switch (this.type_) { case Blockly.ASTNode.types.STACK: - var blockPos = this.location_.getRelativeToSurfaceXY(); + var block = /** @type {!Blockly.Block} */ (this.location_); + var blockPos = block.getRelativeToSurfaceXY(); // TODO: Make sure this is in the bounds of the workspace. var wsCoordinate = new Blockly.utils.Coordinate( blockPos.x, blockPos.y + Blockly.ASTNode.DEFAULT_OFFSET_Y); - return Blockly.ASTNode.createWorkspaceNode( - this.location_.workspace, wsCoordinate); + return Blockly.ASTNode.createWorkspaceNode(block.workspace, wsCoordinate); case Blockly.ASTNode.types.OUTPUT: - var target = this.location_.targetConnection; + var connection = /** @type {!Blockly.Connection} */ (this.location_); + var target = connection.targetConnection; if (target) { return Blockly.ASTNode.createConnectionNode(target); } - return Blockly.ASTNode.createStackNode(this.location_.getSourceBlock()); + return Blockly.ASTNode.createStackNode(connection.getSourceBlock()); case Blockly.ASTNode.types.FIELD: - return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock()); + var field = /** @type {!Blockly.Field} */ (this.location_); + return Blockly.ASTNode.createBlockNode(field.getSourceBlock()); case Blockly.ASTNode.types.INPUT: - return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock()); + var connection = /** @type {!Blockly.Connection} */ (this.location_); + return Blockly.ASTNode.createBlockNode(connection.getSourceBlock()); case Blockly.ASTNode.types.BLOCK: var block = /** @type {!Blockly.Block} */ (this.location_); return this.getOutAstNodeForBlock_(block); case Blockly.ASTNode.types.PREVIOUS: - return this.getOutAstNodeForBlock_(this.location_.getSourceBlock()); + var connection = /** @type {!Blockly.Connection} */ (this.location_); + return this.getOutAstNodeForBlock_(connection.getSourceBlock()); case Blockly.ASTNode.types.NEXT: - return this.getOutAstNodeForBlock_(this.location_.getSourceBlock()); + var connection = /** @type {!Blockly.Connection} */ (this.location_); + return this.getOutAstNodeForBlock_(connection.getSourceBlock()); } return null; diff --git a/core/keyboard_nav/basic_cursor.js b/core/keyboard_nav/basic_cursor.js index 981c8cc89..dca508745 100644 --- a/core/keyboard_nav/basic_cursor.js +++ b/core/keyboard_nav/basic_cursor.js @@ -71,7 +71,7 @@ Blockly.BasicCursor.prototype.prev = function() { return null; } var newNode = this.getPreviousNode_(curNode, this.validNode_); - + if (newNode) { this.setCurNode(newNode); } diff --git a/core/keyboard_nav/cursor.js b/core/keyboard_nav/cursor.js index 1d8b4ae78..ab5aa9218 100644 --- a/core/keyboard_nav/cursor.js +++ b/core/keyboard_nav/cursor.js @@ -19,12 +19,15 @@ goog.require('Blockly.Marker'); goog.require('Blockly.navigation'); goog.require('Blockly.utils.object'); +goog.requireType('Blockly.IBlocklyActionable'); + /** * Class for a cursor. * A cursor controls how a user navigates the Blockly AST. * @constructor * @extends {Blockly.Marker} + * @implements {Blockly.IBlocklyActionable} */ Blockly.Cursor = function() { Blockly.Cursor.superClass_.constructor.call(this); @@ -144,7 +147,8 @@ Blockly.Cursor.prototype.onBlocklyAction = function(action) { // If we are on a field give it the option to handle the action if (this.getCurNode() && this.getCurNode().getType() === Blockly.ASTNode.types.FIELD && - this.getCurNode().getLocation().onBlocklyAction(action)) { + (/** @type {!Blockly.Field} */ (this.getCurNode().getLocation())) + .onBlocklyAction(action)) { return true; } switch (action.name) { diff --git a/core/keyboard_nav/key_map.js b/core/keyboard_nav/key_map.js index e0c4b7957..9ae9bc1b0 100644 --- a/core/keyboard_nav/key_map.js +++ b/core/keyboard_nav/key_map.js @@ -101,7 +101,7 @@ Blockly.user.keyMap.getKeyByAction = function(action) { /** * Serialize the key event. - * @param {!Event} e A key up event holding the key code. + * @param {!KeyboardEvent} e A key up event holding the key code. * @return {string} A string containing the serialized key event. * @package */ diff --git a/core/keyboard_nav/marker.js b/core/keyboard_nav/marker.js index 069475985..2394e393e 100644 --- a/core/keyboard_nav/marker.js +++ b/core/keyboard_nav/marker.js @@ -23,7 +23,6 @@ goog.require('Blockly.navigation'); * @constructor */ Blockly.Marker = function() { - /** * The colour of the marker. * @type {?string} @@ -119,4 +118,3 @@ Blockly.Marker.prototype.dispose = function() { this.getDrawer().dispose(); } }; - diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js index 385ef676a..914882592 100644 --- a/core/keyboard_nav/navigation.js +++ b/core/keyboard_nav/navigation.js @@ -18,7 +18,6 @@ goog.require('Blockly.ASTNode'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.user.keyMap'); - /** * A function to call to give feedback to the user about logs, warnings, and * errors. You can override this to customize feedback (e.g. warning sounds, @@ -102,10 +101,19 @@ Blockly.navigation.MARKER_NAME = 'local_marker_1'; /** * Get the local marker. - * @return {!Blockly.Marker} The local marker for the main workspace. + * @return {Blockly.Marker} The local marker for the main workspace. */ Blockly.navigation.getMarker = function() { - return Blockly.getMainWorkspace().getMarker(Blockly.navigation.MARKER_NAME); + return Blockly.navigation.getNavigationWorkspace() + .getMarker(Blockly.navigation.MARKER_NAME); +}; + +/** + * Get the workspace that is being navigated. + * @return {!Blockly.WorkspaceSvg} The workspace being navigated. + */ +Blockly.navigation.getNavigationWorkspace = function() { + return /** @type {!Blockly.WorkspaceSvg} */ (Blockly.getMainWorkspace()); }; /** @@ -114,8 +122,7 @@ Blockly.navigation.getMarker = function() { * @private */ Blockly.navigation.focusToolbox_ = function() { - var workspace = Blockly.getMainWorkspace(); - var toolbox = workspace.getToolbox(); + var toolbox = Blockly.navigation.getNavigationWorkspace().getToolbox(); if (toolbox) { Blockly.navigation.currentState_ = Blockly.navigation.STATE_TOOLBOX; Blockly.navigation.resetFlyout_(false /* shouldHide */); @@ -134,9 +141,9 @@ Blockly.navigation.focusToolbox_ = function() { Blockly.navigation.focusFlyout_ = function() { var topBlock = null; Blockly.navigation.currentState_ = Blockly.navigation.STATE_FLYOUT; - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var toolbox = workspace.getToolbox(); - var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout(); + var flyout = toolbox ? toolbox.getFlyout() : workspace.getFlyout(); if (!Blockly.navigation.getMarker().getCurNode()) { Blockly.navigation.markAtCursor_(); @@ -159,7 +166,7 @@ Blockly.navigation.focusFlyout_ = function() { */ Blockly.navigation.focusWorkspace_ = function() { Blockly.hideChaff(); - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var cursor = workspace.getCursor(); var reset = !!workspace.getToolbox(); var topBlocks = workspace.getTopBlocks(true); @@ -167,7 +174,7 @@ Blockly.navigation.focusWorkspace_ = function() { Blockly.navigation.resetFlyout_(reset); Blockly.navigation.currentState_ = Blockly.navigation.STATE_WS; if (topBlocks.length > 0) { - cursor.setCurNode(Blockly.navigation.getTopNode(topBlocks[0])); + cursor.setCurNode(Blockly.ASTNode.createTopNode(topBlocks[0])); } else { // TODO: Find the center of the visible workspace. var wsCoord = new Blockly.utils.Coordinate(100, 100); @@ -186,14 +193,14 @@ Blockly.navigation.focusWorkspace_ = function() { * @private */ Blockly.navigation.getFlyoutCursor_ = function() { - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var cursor = null; if (workspace.rendered) { var toolbox = workspace.getToolbox(); - var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout(); - cursor = flyout ? flyout.workspace_.getCursor() : null; + var flyout = toolbox ? toolbox.getFlyout() : workspace.getFlyout(); + cursor = flyout ? flyout.getWorkspace().getCursor() : null; } - return cursor; + return /** @type {Blockly.FlyoutCursor} */ (cursor); }; /** @@ -202,7 +209,7 @@ Blockly.navigation.getFlyoutCursor_ = function() { * it on the workspace. */ Blockly.navigation.insertFromFlyout = function() { - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var flyout = workspace.getFlyout(); if (!flyout || !flyout.isVisible()) { Blockly.navigation.warn_('Trying to insert from the flyout when the flyout does not ' + @@ -210,7 +217,8 @@ Blockly.navigation.insertFromFlyout = function() { return; } - var curBlock = Blockly.navigation.getFlyoutCursor_().getCurNode().getLocation(); + var curBlock = /** @type {!Blockly.BlockSvg} */ ( + Blockly.navigation.getFlyoutCursor_().getCurNode().getLocation()); if (!curBlock.isEnabled()) { Blockly.navigation.warn_('Can\'t insert a disabled block.'); return; @@ -230,7 +238,7 @@ Blockly.navigation.insertFromFlyout = function() { } Blockly.navigation.focusWorkspace_(); - workspace.getCursor().setCurNode(Blockly.navigation.getTopNode(newBlock)); + workspace.getCursor().setCurNode(Blockly.ASTNode.createTopNode(newBlock)); Blockly.navigation.removeMark_(); }; @@ -243,7 +251,7 @@ Blockly.navigation.resetFlyout_ = function(shouldHide) { if (Blockly.navigation.getFlyoutCursor_()) { Blockly.navigation.getFlyoutCursor_().hide(); if (shouldHide) { - Blockly.getMainWorkspace().getFlyout().hide(); + Blockly.navigation.getNavigationWorkspace().getFlyout().hide(); } } }; @@ -260,7 +268,8 @@ Blockly.navigation.resetFlyout_ = function(shouldHide) { */ Blockly.navigation.modifyWarn_ = function() { var markerNode = Blockly.navigation.getMarker().getCurNode(); - var cursorNode = Blockly.getMainWorkspace().getCursor().getCurNode(); + var cursorNode = Blockly.navigation.getNavigationWorkspace() + .getCursor().getCurNode(); if (!markerNode) { Blockly.navigation.warn_('Cannot insert with no marked node.'); @@ -300,7 +309,7 @@ Blockly.navigation.modifyWarn_ = function() { /** * Disconnect the block from its parent and move to the position of the * workspace node. - * @param {Blockly.Block} block The block to be moved to the workspace. + * @param {Blockly.BlockSvg} block The block to be moved to the workspace. * @param {!Blockly.ASTNode} wsNode The workspace node holding the position the * block will be moved to. * @return {boolean} True if the block can be moved to the workspace, @@ -331,7 +340,8 @@ Blockly.navigation.moveBlockToWorkspace_ = function(block, wsNode) { */ Blockly.navigation.modify_ = function() { var markerNode = Blockly.navigation.getMarker().getCurNode(); - var cursorNode = Blockly.getMainWorkspace().getCursor().getCurNode(); + var cursorNode = Blockly.navigation.getNavigationWorkspace() + .getCursor().getCurNode(); if (!Blockly.navigation.modifyWarn_()) { return false; } @@ -343,18 +353,19 @@ Blockly.navigation.modify_ = function() { var markerLoc = markerNode.getLocation(); if (markerNode.isConnection() && cursorNode.isConnection()) { - cursorLoc = /** @type {!Blockly.Connection} */ (cursorLoc); - markerLoc = /** @type {!Blockly.Connection} */ (markerLoc); + cursorLoc = /** @type {!Blockly.RenderedConnection} */ (cursorLoc); + markerLoc = /** @type {!Blockly.RenderedConnection} */ (markerLoc); return Blockly.navigation.connect_(cursorLoc, markerLoc); } else if (markerNode.isConnection() && (cursorType == Blockly.ASTNode.types.BLOCK || cursorType == Blockly.ASTNode.types.STACK)) { - cursorLoc = /** @type {!Blockly.Block} */ (cursorLoc); - markerLoc = /** @type {!Blockly.Connection} */ (markerLoc); + cursorLoc = /** @type {!Blockly.BlockSvg} */ (cursorLoc); + markerLoc = /** @type {!Blockly.RenderedConnection} */ (markerLoc); return Blockly.navigation.insertBlock(cursorLoc, markerLoc); } else if (markerType == Blockly.ASTNode.types.WORKSPACE) { var block = cursorNode ? cursorNode.getSourceBlock() : null; - return Blockly.navigation.moveBlockToWorkspace_(block, markerNode); + return Blockly.navigation.moveBlockToWorkspace_( + /** @type {Blockly.BlockSvg} */ (block), markerNode); } Blockly.navigation.warn_('Unexpected state in Blockly.navigation.modify_.'); return false; @@ -363,9 +374,10 @@ Blockly.navigation.modify_ = function() { /** * If one of the connections source blocks is a child of the other, disconnect * the child. - * @param {!Blockly.Connection} movingConnection The connection that is being - * moved. - * @param {!Blockly.Connection} destConnection The connection to be moved to. + * @param {!Blockly.RenderedConnection} movingConnection The connection that is + * being moved. + * @param {!Blockly.RenderedConnection} destConnection The connection to be + * moved to. * @private */ Blockly.navigation.disconnectChild_ = function(movingConnection, destConnection) { @@ -384,9 +396,10 @@ Blockly.navigation.disconnectChild_ = function(movingConnection, destConnection) /** * If the two blocks are compatible move the moving connection to the target * connection and connect them. - * @param {Blockly.Connection} movingConnection The connection that is being - * moved. - * @param {Blockly.Connection} destConnection The connection to be moved to. + * @param {Blockly.RenderedConnection} movingConnection The connection that is + * being moved. + * @param {Blockly.RenderedConnection} destConnection The connection to be moved + * to. * @return {boolean} True if the connections were connected, false otherwise. * @private */ @@ -414,8 +427,10 @@ Blockly.navigation.moveAndConnect_ = function(movingConnection, destConnection) /** * If the given connection is superior find the inferior connection on the * source block. - * @param {Blockly.Connection} connection The connection trying to be connected. - * @return {Blockly.Connection} The inferior connection or null if none exists. + * @param {Blockly.RenderedConnection} connection The connection trying to be + * connected. + * @return {Blockly.RenderedConnection} The inferior connection or null if none + * exists. * @private */ Blockly.navigation.getInferiorConnection_ = function(connection) { @@ -434,8 +449,10 @@ Blockly.navigation.getInferiorConnection_ = function(connection) { /** * If the given connection is inferior tries to find a superior connection to * connect to. - * @param {Blockly.Connection} connection The connection trying to be connected. - * @return {Blockly.Connection} The superior connection or null if none exists. + * @param {Blockly.RenderedConnection} connection The connection trying to be + * connected. + * @return {Blockly.RenderedConnection} The superior connection or null if none + * exists. * @private */ Blockly.navigation.getSuperiorConnection_ = function(connection) { @@ -453,9 +470,10 @@ Blockly.navigation.getSuperiorConnection_ = function(connection) { * If the given connections are not compatible try finding compatible connections * on the source blocks of the given connections. * - * @param {Blockly.Connection} movingConnection The connection that is being - * moved. - * @param {Blockly.Connection} destConnection The connection to be moved to. + * @param {Blockly.RenderedConnection} movingConnection The connection that is + * being moved. + * @param {Blockly.RenderedConnection} destConnection The connection to be moved + * to. * @return {boolean} True if the two connections or their target connections * were connected, false otherwise. * @private @@ -495,8 +513,9 @@ Blockly.navigation.connect_ = function(movingConnection, destConnection) { /** * Tries to connect the given block to the destination connection, making an * intelligent guess about which connection to use to on the moving block. - * @param {!Blockly.Block} block The block to move. - * @param {!Blockly.Connection} destConnection The connection to connect to. + * @param {!Blockly.BlockSvg} block The block to move. + * @param {!Blockly.RenderedConnection} destConnection The connection to connect + * to. * @return {boolean} Whether the connection was successful. */ Blockly.navigation.insertBlock = function(block, destConnection) { @@ -518,7 +537,8 @@ Blockly.navigation.insertBlock = function(block, destConnection) { break; case Blockly.OUTPUT_VALUE: for (var i = 0; i < block.inputList.length; i++) { - var inputConnection = block.inputList[i].connection; + var inputConnection = /** @type {Blockly.RenderedConnection} */ ( + block.inputList[i].connection); if (inputConnection && inputConnection.type === Blockly.INPUT_VALUE && Blockly.navigation.connect_(inputConnection, destConnection)) { return true; @@ -543,13 +563,14 @@ Blockly.navigation.insertBlock = function(block, destConnection) { * @private */ Blockly.navigation.disconnectBlocks_ = function() { - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var curNode = workspace.getCursor().getCurNode(); if (!curNode.isConnection()) { Blockly.navigation.log_('Cannot disconnect blocks when the cursor is not on a connection'); return; } - var curConnection = /** @type {!Blockly.Connection} */ (curNode.getLocation()); + var curConnection = + /** @type {!Blockly.RenderedConnection} */ (curNode.getLocation()); if (!curConnection.isConnected()) { Blockly.navigation.log_('Cannot disconnect unconnected connection'); return; @@ -584,7 +605,7 @@ Blockly.navigation.disconnectBlocks_ = function() { */ Blockly.navigation.markAtCursor_ = function() { Blockly.navigation.getMarker().setCurNode( - Blockly.getMainWorkspace().getCursor().getCurNode()); + Blockly.navigation.getNavigationWorkspace().getCursor().getCurNode()); }; /** @@ -606,31 +627,12 @@ Blockly.navigation.setState = function(newState) { Blockly.navigation.currentState_ = newState; }; -/** - * Gets the top node on a block. - * This is either the previous connection, output connection or the block. - * @param {!Blockly.Block} block The block to find the top most AST node on. - * @return {Blockly.ASTNode} The AST node holding the top most node on the - * block. - * @package - */ -Blockly.navigation.getTopNode = function(block) { - var astNode; - var topConnection = block.previousConnection || block.outputConnection; - if (topConnection) { - astNode = Blockly.ASTNode.createConnectionNode(topConnection); - } else { - astNode = Blockly.ASTNode.createBlockNode(block); - } - return astNode; -}; - /** * Before a block is deleted move the cursor to the appropriate position. - * @param {!Blockly.Block} deletedBlock The block that is being deleted. + * @param {!Blockly.BlockSvg} deletedBlock The block that is being deleted. */ Blockly.navigation.moveCursorOnBlockDelete = function(deletedBlock) { - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); if (!workspace) { return; } @@ -664,11 +666,11 @@ Blockly.navigation.moveCursorOnBlockDelete = function(deletedBlock) { /** * When a block that the cursor is on is mutated move the cursor to the block * level. - * @param {!Blockly.Block} mutatedBlock The block that is being mutated. + * @param {!Blockly.BlockSvg} mutatedBlock The block that is being mutated. * @package */ Blockly.navigation.moveCursorOnBlockMutation = function(mutatedBlock) { - var cursor = Blockly.getMainWorkspace().getCursor(); + var cursor = Blockly.navigation.getNavigationWorkspace().getCursor(); if (cursor) { var curNode = cursor.getCurNode(); var block = curNode ? curNode.getSourceBlock() : null; @@ -683,8 +685,9 @@ Blockly.navigation.moveCursorOnBlockMutation = function(mutatedBlock) { * Enable accessibility mode. */ Blockly.navigation.enableKeyboardAccessibility = function() { - if (!Blockly.getMainWorkspace().keyboardAccessibilityMode) { - Blockly.getMainWorkspace().keyboardAccessibilityMode = true; + var workspace = Blockly.navigation.getNavigationWorkspace(); + if (!workspace.keyboardAccessibilityMode) { + workspace.keyboardAccessibilityMode = true; Blockly.navigation.focusWorkspace_(); } }; @@ -693,9 +696,9 @@ Blockly.navigation.enableKeyboardAccessibility = function() { * Disable accessibility mode. */ Blockly.navigation.disableKeyboardAccessibility = function() { - if (Blockly.getMainWorkspace().keyboardAccessibilityMode) { - var workspace = Blockly.getMainWorkspace(); - Blockly.getMainWorkspace().keyboardAccessibilityMode = false; + var workspace = Blockly.navigation.getNavigationWorkspace(); + if (workspace.keyboardAccessibilityMode) { + workspace.keyboardAccessibilityMode = false; workspace.getCursor().hide(); Blockly.navigation.getMarker().hide(); if (Blockly.navigation.getFlyoutCursor_()) { @@ -752,7 +755,7 @@ Blockly.navigation.error_ = function(msg) { /** * Handler for all the keyboard navigation events. - * @param {!Event} e The keyboard event. + * @param {!KeyboardEvent} e The keyboard event. * @return {boolean} True if the key was handled false otherwise. */ Blockly.navigation.onKeyPress = function(e) { @@ -772,10 +775,11 @@ Blockly.navigation.onKeyPress = function(e) { * @return {boolean} True if the action has been handled, false otherwise. */ Blockly.navigation.onBlocklyAction = function(action) { - var readOnly = Blockly.getMainWorkspace().options.readOnly; + var workspace = Blockly.navigation.getNavigationWorkspace(); + var readOnly = workspace.options.readOnly; var actionHandled = false; - if (Blockly.getMainWorkspace().keyboardAccessibilityMode) { + if (workspace.keyboardAccessibilityMode) { if (!readOnly) { actionHandled = Blockly.navigation.handleActions_(action); // If in readonly mode only handle valid actions. @@ -818,9 +822,9 @@ Blockly.navigation.handleActions_ = function(action) { * @private */ Blockly.navigation.flyoutOnAction_ = function(action) { - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var toolbox = workspace.getToolbox(); - var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout(); + var flyout = toolbox ? toolbox.getFlyout() : workspace.getFlyout(); if (flyout && flyout.onBlocklyAction(action)) { return true; @@ -848,9 +852,10 @@ Blockly.navigation.flyoutOnAction_ = function(action) { * @private */ Blockly.navigation.toolboxOnAction_ = function(action) { - var workspace = Blockly.getMainWorkspace(); + var workspace = Blockly.navigation.getNavigationWorkspace(); var toolbox = workspace.getToolbox(); - var handled = toolbox ? toolbox.onBlocklyAction(action) : false; + var handled = toolbox && typeof toolbox.onBlocklyAction == 'function' ? + toolbox.onBlocklyAction(action) : false; if (handled) { return true; @@ -881,8 +886,9 @@ Blockly.navigation.toolboxOnAction_ = function(action) { * @private */ Blockly.navigation.moveWSCursor_ = function(xDirection, yDirection) { - var cursor = Blockly.getMainWorkspace().getCursor(); - var curNode = Blockly.getMainWorkspace().getCursor().getCurNode(); + var workspace = Blockly.navigation.getNavigationWorkspace(); + var cursor = workspace.getCursor(); + var curNode = workspace.getCursor().getCurNode(); if (curNode.getType() !== Blockly.ASTNode.types.WORKSPACE) { return false; @@ -893,7 +899,7 @@ Blockly.navigation.moveWSCursor_ = function(xDirection, yDirection) { var newY = yDirection * Blockly.navigation.WS_MOVE_DISTANCE + wsCoord.y; cursor.setCurNode(Blockly.ASTNode.createWorkspaceNode( - Blockly.getMainWorkspace(), new Blockly.utils.Coordinate(newX, newY))); + workspace, new Blockly.utils.Coordinate(newX, newY))); return true; }; @@ -904,7 +910,8 @@ Blockly.navigation.moveWSCursor_ = function(xDirection, yDirection) { * @private */ Blockly.navigation.workspaceOnAction_ = function(action) { - if (Blockly.getMainWorkspace().getCursor().onBlocklyAction(action)) { + var workspace = Blockly.navigation.getNavigationWorkspace(); + if (workspace.getCursor().onBlocklyAction(action)) { return true; } switch (action.name) { @@ -935,11 +942,11 @@ Blockly.navigation.workspaceOnAction_ = function(action) { * @private */ Blockly.navigation.handleEnterForWS_ = function() { - var cursor = Blockly.getMainWorkspace().getCursor(); + var cursor = Blockly.navigation.getNavigationWorkspace().getCursor(); var curNode = cursor.getCurNode(); var nodeType = curNode.getType(); if (nodeType == Blockly.ASTNode.types.FIELD) { - curNode.getLocation().showEditor(); + (/** @type {!Blockly.Field} */(curNode.getLocation())).showEditor(); } else if (curNode.isConnection() || nodeType == Blockly.ASTNode.types.WORKSPACE) { Blockly.navigation.markAtCursor_(); diff --git a/core/keyboard_nav/tab_navigate_cursor.js b/core/keyboard_nav/tab_navigate_cursor.js index 5e85136a9..9b403bd0a 100644 --- a/core/keyboard_nav/tab_navigate_cursor.js +++ b/core/keyboard_nav/tab_navigate_cursor.js @@ -38,10 +38,9 @@ Blockly.TabNavigateCursor.prototype.validNode_ = function(node) { var isValid = false; var type = node && node.getType(); if (node) { - var location = node.getLocation(); + var location = /** @type {Blockly.Field} */ (node.getLocation()); if (type == Blockly.ASTNode.types.FIELD && - location && location.isTabNavigable() && - (/** @type {!Blockly.Field} */ (location)).isClickable()) { + location && location.isTabNavigable() && location.isClickable()) { isValid = true; } } diff --git a/core/menu.js b/core/menu.js new file mode 100644 index 000000000..a1621e83b --- /dev/null +++ b/core/menu.js @@ -0,0 +1,465 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Blockly menu similar to Closure's goog.ui.Menu + * @author samelh@google.com (Sam El-Husseini) + */ +'use strict'; + +goog.provide('Blockly.Menu'); + +goog.require('Blockly.utils.aria'); +goog.require('Blockly.utils.Coordinate'); +goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.KeyCodes'); +goog.require('Blockly.utils.style'); + + +/** + * A basic menu class. + * @constructor + */ +Blockly.Menu = function() { + /** + * Array of menu items. + * (Nulls are never in the array, but typing the array as nullable prevents + * the compiler from objecting to .indexOf(null)) + * @type {!Array.} + * @private + */ + this.menuItems_ = []; + + /** + * Coordinates of the mousedown event that caused this menu to open. Used to + * prevent the consequent mouseup event due to a simple click from activating + * a menu item immediately. + * @type {?Blockly.utils.Coordinate} + * @package + */ + this.openingCoords = null; + + /** + * This is the element that we will listen to the real focus events on. + * A value of null means no menu item is highlighted. + * @type {Blockly.MenuItem} + * @private + */ + this.highlightedItem_ = null; + + /** + * Mouse over event data. + * @type {?Blockly.EventData} + * @private + */ + this.mouseOverHandler_ = null; + + /** + * Click event data. + * @type {?Blockly.EventData} + * @private + */ + this.clickHandler_ = null; + + /** + * Mouse enter event data. + * @type {?Blockly.EventData} + * @private + */ + this.mouseEnterHandler_ = null; + + /** + * Mouse leave event data. + * @type {?Blockly.EventData} + * @private + */ + this.mouseLeaveHandler_ = null; + + /** + * Key down event data. + * @type {?Blockly.EventData} + * @private + */ + this.onKeyDownHandler_ = null; + + /** + * The menu's root DOM element. + * @type {Element} + * @private + */ + this.element_ = null; + + /** + * ARIA name for this menu. + * @type {?Blockly.utils.aria.Role} + * @private + */ + this.roleName_ = null; +}; + + +/** + * Add a new menu item to the bottom of this menu. + * @param {!Blockly.MenuItem} menuItem Menu item to append. + */ +Blockly.Menu.prototype.addChild = function(menuItem) { + this.menuItems_.push(menuItem); +}; + +/** + * Creates the menu DOM. + * @param {!Element} container Element upon which to append this menu. + */ +Blockly.Menu.prototype.render = function(container) { + var element = /** @type {!HTMLDivElement} */ (document.createElement('div')); + // goog-menu is deprecated, use blocklyMenu. May 2020. + element.className = 'blocklyMenu goog-menu blocklyNonSelectable'; + element.tabIndex = 0; + if (this.roleName_) { + Blockly.utils.aria.setRole(element, this.roleName_); + } + this.element_ = element; + + // Add menu items. + for (var i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) { + element.appendChild(menuItem.createDom()); + } + + // Add event handlers. + this.mouseOverHandler_ = Blockly.bindEventWithChecks_(element, + 'mouseover', this, this.handleMouseOver_, true); + this.clickHandler_ = Blockly.bindEventWithChecks_(element, + 'click', this, this.handleClick_, true); + this.mouseEnterHandler_ = Blockly.bindEventWithChecks_(element, + 'mouseenter', this, this.handleMouseEnter_, true); + this.mouseLeaveHandler_ = Blockly.bindEventWithChecks_(element, + 'mouseleave', this, this.handleMouseLeave_, true); + this.onKeyDownHandler_ = Blockly.bindEventWithChecks_(element, + 'keydown', this, this.handleKeyEvent_); + + container.appendChild(element); +}; + +/** + * Gets the menu's element. + * @return {Element} The DOM element. + * @package + */ +Blockly.Menu.prototype.getElement = function() { + return this.element_; +}; + +/** + * Focus the menu element. + * @package + */ +Blockly.Menu.prototype.focus = function() { + var el = this.getElement(); + if (el) { + el.focus({preventScroll:true}); + Blockly.utils.dom.addClass(el, 'blocklyFocused'); + } +}; + +/** + * Blur the menu element. + * @private + */ +Blockly.Menu.prototype.blur_ = function() { + var el = this.getElement(); + if (el) { + el.blur(); + Blockly.utils.dom.removeClass(el, 'blocklyFocused'); + } +}; + +/** + * Set the menu accessibility role. + * @param {!Blockly.utils.aria.Role} roleName role name. + * @package + */ +Blockly.Menu.prototype.setRole = function(roleName) { + this.roleName_ = roleName; +}; + +/** + * Dispose of this menu. + */ +Blockly.Menu.prototype.dispose = function() { + // Remove event handlers. + if (this.mouseOverHandler_) { + Blockly.unbindEvent_(this.mouseOverHandler_); + this.mouseOverHandler_ = null; + } + if (this.clickHandler_) { + Blockly.unbindEvent_(this.clickHandler_); + this.clickHandler_ = null; + } + if (this.mouseEnterHandler_) { + Blockly.unbindEvent_(this.mouseEnterHandler_); + this.mouseEnterHandler_ = null; + } + if (this.mouseLeaveHandler_) { + Blockly.unbindEvent_(this.mouseLeaveHandler_); + this.mouseLeaveHandler_ = null; + } + if (this.onKeyDownHandler_) { + Blockly.unbindEvent_(this.onKeyDownHandler_); + this.onKeyDownHandler_ = null; + } + + // Remove menu items. + for (var i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) { + menuItem.dispose(); + } + this.element_ = null; +}; + +// Child component management. + +/** + * Returns the child menu item that owns the given DOM element, + * or null if no such menu item is found. + * @param {Element} elem DOM element whose owner is to be returned. + * @return {?Blockly.MenuItem} Menu item for which the DOM element belongs to. + * @private + */ +Blockly.Menu.prototype.getMenuItem_ = function(elem) { + var menuElem = this.getElement(); + // Node might be the menu border (resulting in no associated menu item), or + // a menu item's div, or some element within the menu item. + // Walk up parents until one meets either the menu's root element, or + // a menu item's div. + while (elem && elem != menuElem) { + if (Blockly.utils.dom.hasClass(elem, 'blocklyMenuItem')) { + // Having found a menu item's div, locate that menu item in this menu. + for (var i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) { + if (menuItem.getElement() == elem) { + return menuItem; + } + } + } + elem = elem.parentElement; + } + return null; +}; + +// Highlight management. + +/** + * Highlights the given menu item, or clears highlighting if null. + * @param {Blockly.MenuItem} item Item to highlight, or null. + * @package + */ +Blockly.Menu.prototype.setHighlighted = function(item) { + var currentHighlighted = this.highlightedItem_; + if (currentHighlighted) { + currentHighlighted.setHighlighted(false); + this.highlightedItem_ = null; + } + if (item) { + item.setHighlighted(true); + this.highlightedItem_ = item; + // Bring the highlighted item into view. This has no effect if the menu is + // not scrollable. + var el = /** @type {!Element} */ (this.getElement()); + Blockly.utils.style.scrollIntoContainerView( + /** @type {!Element} */ (item.getElement()), el); + + Blockly.utils.aria.setState(el, Blockly.utils.aria.State.ACTIVEDESCENDANT, + item.getId()); + } +}; + +/** + * Highlights the next highlightable item (or the first if nothing is currently + * highlighted). + * @package + */ +Blockly.Menu.prototype.highlightNext = function() { + var index = this.menuItems_.indexOf(this.highlightedItem_); + this.highlightHelper_(index, 1); +}; + +/** + * Highlights the previous highlightable item (or the last if nothing is + * currently highlighted). + * @package + */ +Blockly.Menu.prototype.highlightPrevious = function() { + var index = this.menuItems_.indexOf(this.highlightedItem_); + this.highlightHelper_(index < 0 ? this.menuItems_.length : index, -1); +}; + +/** + * Highlights the first highlightable item. + * @private + */ +Blockly.Menu.prototype.highlightFirst_ = function() { + this.highlightHelper_(-1, 1); +}; + +/** + * Highlights the last highlightable item. + * @private + */ +Blockly.Menu.prototype.highlightLast_ = function() { + this.highlightHelper_(this.menuItems_.length, -1); +}; + +/** + * Helper function that manages the details of moving the highlight among + * child menuitems in response to keyboard events. + * @param {number} startIndex Start index. + * @param {number} delta Step direction: 1 to go down, -1 to go up. + * @private + */ +Blockly.Menu.prototype.highlightHelper_ = function(startIndex, delta) { + var index = startIndex + delta; + var menuItem; + while ((menuItem = this.menuItems_[index])) { + if (menuItem.isEnabled()) { + this.setHighlighted(menuItem); + break; + } + index += delta; + } +}; + +// Mouse events. + +/** + * Handles mouseover events. Highlight menuitems as the user hovers over them. + * @param {!Event} e Mouse event to handle. + * @private + */ +Blockly.Menu.prototype.handleMouseOver_ = function(e) { + var menuItem = this.getMenuItem_(/** @type {Element} */ (e.target)); + + if (menuItem) { + if (menuItem.isEnabled()) { + if (this.highlightedItem_ != menuItem) { + this.setHighlighted(menuItem); + } + } else { + this.setHighlighted(null); + } + } +}; + +/** + * Handles click events. Pass the event onto the child menuitem to handle. + * @param {!Event} e Click event to handle. + * @private + */ +Blockly.Menu.prototype.handleClick_ = function(e) { + var oldCoords = this.openingCoords; + // Clear out the saved opening coords immediately so they're not used twice. + this.openingCoords = null; + if (oldCoords && typeof e.clientX == 'number') { + var newCoords = new Blockly.utils.Coordinate(e.clientX, e.clientY); + if (Blockly.utils.Coordinate.distance(oldCoords, newCoords) < 1) { + // This menu was opened by a mousedown and we're handling the consequent + // click event. The coords haven't changed, meaning this was the same + // opening event. Don't do the usual behavior because the menu just popped + // up under the mouse and the user didn't mean to activate this item. + return; + } + } + + var menuItem = this.getMenuItem_(/** @type {Element} */ (e.target)); + if (menuItem) { + menuItem.performAction(); + } +}; + +/** + * Handles mouse enter events. Focus the element. + * @param {Event} _e Mouse event to handle. + * @private + */ +Blockly.Menu.prototype.handleMouseEnter_ = function(_e) { + this.focus(); +}; + +/** + * Handles mouse leave events. Blur and clear highlight. + * @param {Event} _e Mouse event to handle. + * @private + */ +Blockly.Menu.prototype.handleMouseLeave_ = function(_e) { + if (this.getElement()) { + this.blur_(); + this.setHighlighted(null); + } +}; + +// Keyboard events. + +/** + * Attempts to handle a keyboard event, if the menu item is enabled, by calling + * {@link handleKeyEventInternal_}. + * @param {!Event} e Key event to handle. + * @private + */ +Blockly.Menu.prototype.handleKeyEvent_ = function(e) { + if (!this.menuItems_.length) { + // Empty menu. + return; + } + if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) { + // Do not handle the key event if any modifier key is pressed. + return; + } + + var highlighted = this.highlightedItem_; + switch (e.keyCode) { + case Blockly.utils.KeyCodes.ENTER: + case Blockly.utils.KeyCodes.SPACE: + if (highlighted) { + highlighted.performAction(); + } + break; + + case Blockly.utils.KeyCodes.UP: + this.highlightPrevious(); + break; + + case Blockly.utils.KeyCodes.DOWN: + this.highlightNext(); + break; + + case Blockly.utils.KeyCodes.PAGE_UP: + case Blockly.utils.KeyCodes.HOME: + this.highlightFirst_(); + break; + + case Blockly.utils.KeyCodes.PAGE_DOWN: + case Blockly.utils.KeyCodes.END: + this.highlightLast_(); + break; + + default: + // Not a key the menu is interested in. + return; + } + // The menu used this key, don't let it have secondary effects. + e.preventDefault(); + e.stopPropagation(); +}; + +/** + * Get the size of a rendered menu. + * @return {!Blockly.utils.Size} Object with width and height properties. + * @package + */ +Blockly.Menu.prototype.getSize = function() { + var menuDom = this.getElement(); + var menuSize = Blockly.utils.style.getSize(/** @type {!Element} */ (menuDom)); + // Recalculate height for the total content, not only box height. + menuSize.height = menuDom.scrollHeight; + return menuSize; +}; diff --git a/core/menuitem.js b/core/menuitem.js new file mode 100644 index 000000000..c9ba07981 --- /dev/null +++ b/core/menuitem.js @@ -0,0 +1,276 @@ +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Blockly menu item similar to Closure's goog.ui.MenuItem + * @author samelh@google.com (Sam El-Husseini) + */ +'use strict'; + +goog.provide('Blockly.MenuItem'); + +goog.require('Blockly.utils.aria'); +goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.IdGenerator'); + + +/** + * Class representing an item in a menu. + * + * @param {string} content Text caption to display as the content of + * the item. + * @param {string=} opt_value Data/model associated with the menu item. + * @constructor + */ +Blockly.MenuItem = function(content, opt_value) { + /** + * Human-readable text of this menu item. + * @type {string} + * @private + */ + this.content_ = content; + + /** + * Machine-readable value of this menu item. + * @type {string|undefined} + * @private + */ + this.value_ = opt_value; + + /** + * Is the menu item clickable, as opposed to greyed-out. + * @type {boolean} + * @private + */ + this.enabled_ = true; + + /** + * The DOM element for the menu item. + * @type {?Element} + * @private + */ + this.element_ = null; + + /** + * Whether the menu item is rendered right-to-left. + * @type {boolean} + * @private + */ + this.rightToLeft_ = false; + + /** + * ARIA name for this menu. + * @type {?Blockly.utils.aria.Role} + * @private + */ + this.roleName_ = null; + + /** + * Is this menu item checkable. + * @type {boolean} + * @private + */ + this.checkable_ = false; + + /** + * Is this menu item currently checked. + * @type {boolean} + * @private + */ + this.checked_ = false; + + /** + * Is this menu item currently highlighted. + * @type {boolean} + * @private + */ + this.highlight_ = false; + + /** + * Bound function to call when this menu item is clicked. + * @type {Function} + * @private + */ + this.actionHandler_ = null; +}; + + +/** + * Creates the menuitem's DOM. + * @return {!Element} Completed DOM. + */ +Blockly.MenuItem.prototype.createDom = function() { + var element = document.createElement('div'); + element.id = Blockly.utils.IdGenerator.getNextUniqueId(); + this.element_ = element; + + // Set class and style + // goog-menuitem* is deprecated, use blocklyMenuItem*. May 2020. + element.className = 'blocklyMenuItem goog-menuitem ' + + (this.enabled_ ? '' : 'blocklyMenuItemDisabled goog-menuitem-disabled ') + + (this.checked_ ? 'blocklyMenuItemSelected goog-option-selected ' : '') + + (this.highlight_ ? + 'blocklyMenuItemHighlight goog-menuitem-highlight ' : '') + + (this.rightToLeft_ ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : ''); + + var content = document.createElement('div'); + content.className = 'blocklyMenuItemContent goog-menuitem-content'; + // Add a checkbox for checkable menu items. + if (this.checkable_) { + var checkbox = document.createElement('div'); + checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox'; + content.appendChild(checkbox); + } + + content.appendChild(document.createTextNode(this.content_)); + element.appendChild(content); + + // Initialize ARIA role and state. + if (this.roleName_) { + Blockly.utils.aria.setRole(element, this.roleName_); + } + Blockly.utils.aria.setState(element, Blockly.utils.aria.State.SELECTED, + (this.checkable_ && this.checked_) || false); + Blockly.utils.aria.setState(element, Blockly.utils.aria.State.DISABLED, + !this.enabled_); + + return element; +}; + +/** + * Dispose of this menu item. + */ +Blockly.MenuItem.prototype.dispose = function() { + this.element_ = null; +}; + +/** + * Gets the menu item's element. + * @return {Element} The DOM element. + * @package + */ +Blockly.MenuItem.prototype.getElement = function() { + return this.element_; +}; + +/** + * Gets the unique ID for this menu item. + * @return {string} Unique component ID. + * @package + */ +Blockly.MenuItem.prototype.getId = function() { + return this.element_.id; +}; + +/** + * Gets the value associated with the menu item. + * @return {*} value Value associated with the menu item. + * @package + */ +Blockly.MenuItem.prototype.getValue = function() { + return this.value_; +}; + +/** + * Set menu item's rendering direction. + * @param {boolean} rtl True if RTL, false if LTR. + * @package + */ +Blockly.MenuItem.prototype.setRightToLeft = function(rtl) { + this.rightToLeft_ = rtl; +}; + +/** + * Set the menu item's accessibility role. + * @param {!Blockly.utils.aria.Role} roleName Role name. + * @package + */ +Blockly.MenuItem.prototype.setRole = function(roleName) { + this.roleName_ = roleName; +}; + +/** + * Sets the menu item to be checkable or not. Set to true for menu items + * that represent checkable options. + * @param {boolean} checkable Whether the menu item is checkable. + * @package + */ +Blockly.MenuItem.prototype.setCheckable = function(checkable) { + this.checkable_ = checkable; +}; + +/** + * Checks or unchecks the component. + * @param {boolean} checked Whether to check or uncheck the component. + * @package + */ +Blockly.MenuItem.prototype.setChecked = function(checked) { + this.checked_ = checked; +}; + +/** + * Highlights or unhighlights the component. + * @param {boolean} highlight Whether to highlight or unhighlight the component. + * @package + */ +Blockly.MenuItem.prototype.setHighlighted = function(highlight) { + this.highlight_ = highlight; + + var el = this.getElement(); + if (el && this.isEnabled()) { + // goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight. + // May 2020. + var name = 'blocklyMenuItemHighlight'; + var nameDep = 'goog-menuitem-highlight'; + if (highlight) { + Blockly.utils.dom.addClass(el, name); + Blockly.utils.dom.addClass(el, nameDep); + } else { + Blockly.utils.dom.removeClass(el, name); + Blockly.utils.dom.removeClass(el, nameDep); + } + } +}; + +/** + * Returns true if the menu item is enabled, false otherwise. + * @return {boolean} Whether the menu item is enabled. + * @package + */ +Blockly.MenuItem.prototype.isEnabled = function() { + return this.enabled_; +}; + +/** + * Enables or disables the menu item. + * @param {boolean} enabled Whether to enable or disable the menu item. + * @package + */ +Blockly.MenuItem.prototype.setEnabled = function(enabled) { + this.enabled_ = enabled; +}; + +/** + * Performs the appropriate action when the menu item is activated + * by the user. + * @package + */ +Blockly.MenuItem.prototype.performAction = function() { + if (this.isEnabled() && this.actionHandler_) { + this.actionHandler_(this); + } +}; + +/** + * Set the handler that's called when the menu item is activated by the user. + * `obj` will be used as the 'this' object in the function when called. + * @param {function(!Blockly.MenuItem)} fn The handler. + * @param {!Object} obj Used as the 'this' object in fn when called. + * @package + */ +Blockly.MenuItem.prototype.onAction = function(fn, obj) { + this.actionHandler_ = fn.bind(obj); +}; diff --git a/core/mutator.js b/core/mutator.js index 313e3f101..884f1ca46 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -27,6 +27,8 @@ goog.require('Blockly.utils.xml'); goog.require('Blockly.WorkspaceSvg'); goog.require('Blockly.Xml'); +goog.requireType('Blockly.utils.Metrics'); + /** * Class for a mutator dialog. @@ -74,7 +76,7 @@ Blockly.Mutator.prototype.getWorkspace = function() { /** * Draw the mutator icon. * @param {!Element} group The icon group. - * @private + * @protected */ Blockly.Mutator.prototype.drawIcon_ = function(group) { // Square with rounded corners. @@ -165,8 +167,12 @@ Blockly.Mutator.prototype.createEditor_ = function() { })); workspaceOptions.toolboxPosition = this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT; - workspaceOptions.languageTree = quarkXml; - workspaceOptions.getMetrics = this.getFlyoutMetrics_.bind(this); + var hasFlyout = !!quarkXml; + if (hasFlyout) { + workspaceOptions.languageTree = + Blockly.utils.toolbox.convertToolboxToJSON(quarkXml); + workspaceOptions.getMetrics = this.getFlyoutMetrics_.bind(this); + } this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); this.workspace_.isMutator = true; this.workspace_.addChangeListener(Blockly.Events.disableOrphans); @@ -175,13 +181,15 @@ Blockly.Mutator.prototype.createEditor_ = function() { // a top level svg. Instead of handling scale themselves, mutators // inherit scale from the parent workspace. // To fix this, scale needs to be applied at a different level in the dom. - var flyoutSvg = this.workspace_.addFlyout('g'); + var flyoutSvg = hasFlyout ? this.workspace_.addFlyout('g') : null; var background = this.workspace_.createDom('blocklyMutatorBackground'); - // Insert the flyout after the but before the block canvas so that - // the flyout is underneath in z-order. This makes blocks layering during - // dragging work properly. - background.insertBefore(flyoutSvg, this.workspace_.svgBlockCanvas_); + if (flyoutSvg) { + // Insert the flyout after the but before the block canvas so that + // the flyout is underneath in z-order. This makes blocks layering during + // dragging work properly. + background.insertBefore(flyoutSvg, this.workspace_.svgBlockCanvas_); + } this.svgDialog_.appendChild(background); return this.svgDialog_; @@ -286,7 +294,7 @@ Blockly.Mutator.prototype.setVisible = function(visible) { var flyout = this.workspace_.getFlyout(); if (tree) { flyout.init(this.workspace_); - flyout.show(tree.childNodes); + flyout.show(tree); } this.rootBlock_ = this.block_.decompose(this.workspace_); @@ -378,7 +386,8 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { block.initSvg(); block.render(); - if (Blockly.getMainWorkspace().keyboardAccessibilityMode) { + if ((/** @type {!Blockly.WorkspaceSvg} */ (Blockly.getMainWorkspace())) + .keyboardAccessibilityMode) { Blockly.navigation.moveCursorOnBlockMutation(block); } var newMutationDom = block.mutationToDom(); @@ -386,13 +395,6 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { if (oldMutation != newMutation) { Blockly.Events.fire(new Blockly.Events.BlockChange( block, 'mutation', null, oldMutation, newMutation)); - // Ensure that any bump is part of this mutation's event group. - var group = Blockly.Events.getGroup(); - setTimeout(function() { - Blockly.Events.setGroup(group); - block.bumpNeighbours(); - Blockly.Events.setGroup(false); - }, Blockly.BUMP_DELAY); } // Don't update the bubble until the drag has ended, to avoid moving blocks @@ -411,15 +413,26 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { * .viewWidth: Width of the visible rectangle, * .absoluteTop: Top-edge of view. * .absoluteLeft: Left-edge of view. - * @return {!Object} Contains size and position metrics of mutator dialog's - * workspace. + * @return {!Blockly.utils.Metrics} Contains size and position metrics of + * mutator dialog's workspace. * @private */ Blockly.Mutator.prototype.getFlyoutMetrics_ = function() { + // The mutator workspace only uses a subset of Blockly.utils.Metrics + // properties as features such as scroll and zoom are unsupported. + var unsupported = 0; return { + contentHeight: unsupported, + contentWidth: unsupported, + contentTop: unsupported, + contentLeft: unsupported, + viewHeight: this.workspaceHeight_, viewWidth: this.workspaceWidth_ - this.workspace_.getFlyout().getWidth(), - absoluteTop: 0, + viewTop: unsupported, + viewLeft: unsupported, + + absoluteTop: unsupported, absoluteLeft: this.workspace_.RTL ? 0 : this.workspace_.getFlyout().getWidth() }; diff --git a/core/options.js b/core/options.js index 55406ed72..57e69c5e7 100644 --- a/core/options.js +++ b/core/options.js @@ -14,7 +14,11 @@ goog.provide('Blockly.Options'); goog.require('Blockly.Theme'); goog.require('Blockly.Themes.Classic'); +goog.require('Blockly.registry'); goog.require('Blockly.user.keyMap'); +goog.require('Blockly.utils.IdGenerator'); +goog.require('Blockly.utils.Metrics'); +goog.require('Blockly.utils.toolbox'); goog.require('Blockly.utils.userAgent'); goog.require('Blockly.Xml'); @@ -29,7 +33,7 @@ goog.require('Blockly.Xml'); Blockly.Options = function(options) { var readOnly = !!options['readOnly']; if (readOnly) { - var languageTree = null; + var toolboxContents = null; var hasCategories = false; var hasTrashcan = false; var hasCollapse = false; @@ -37,10 +41,12 @@ Blockly.Options = function(options) { var hasDisable = false; var hasSounds = false; } else { - var languageTree = - Blockly.Options.parseToolboxTree(options['toolbox'] || null); - var hasCategories = Boolean(languageTree && - languageTree.getElementsByTagName('category').length); + var toolboxDef = options['toolbox']; + if (!Array.isArray(toolboxDef)) { + toolboxDef = Blockly.Options.parseToolboxTree(toolboxDef || null); + } + var toolboxContents = Blockly.utils.toolbox.convertToolboxToJSON(toolboxDef); + var hasCategories = Blockly.utils.toolbox.hasCategories(toolboxContents); var hasTrashcan = options['trashcan']; if (hasTrashcan === undefined) { hasTrashcan = hasCategories; @@ -106,39 +112,65 @@ Blockly.Options = function(options) { var renderer = options['renderer'] || 'geras'; + var plugins = options['plugins'] || {}; + + /** @type {boolean} */ this.RTL = rtl; + /** @type {boolean} */ this.oneBasedIndex = oneBasedIndex; + /** @type {boolean} */ this.collapse = hasCollapse; + /** @type {boolean} */ this.comments = hasComments; + /** @type {boolean} */ this.disable = hasDisable; + /** @type {boolean} */ this.readOnly = readOnly; + /** @type {number} */ this.maxBlocks = options['maxBlocks'] || Infinity; + /** @type {?Object.} */ this.maxInstances = options['maxInstances']; + /** @type {string} */ this.pathToMedia = pathToMedia; + /** @type {boolean} */ this.hasCategories = hasCategories; + /** @type {!Object} */ this.moveOptions = Blockly.Options.parseMoveOptions(options, hasCategories); /** @deprecated January 2019 */ this.hasScrollbars = this.moveOptions.scrollbars; + /** @type {boolean} */ this.hasTrashcan = hasTrashcan; + /** @type {number} */ this.maxTrashcanContents = maxTrashcanContents; + /** @type {boolean} */ this.hasSounds = hasSounds; + /** @type {boolean} */ this.hasCss = hasCss; + /** @type {boolean} */ this.horizontalLayout = horizontalLayout; - this.languageTree = languageTree; + /** @type {Array.} */ + this.languageTree = toolboxContents; + /** @type {!Object} */ this.gridOptions = Blockly.Options.parseGridOptions_(options); + /** @type {!Object} */ this.zoomOptions = Blockly.Options.parseZoomOptions_(options); + /** @type {number} */ this.toolboxPosition = toolboxPosition; + /** @type {!Blockly.Theme} */ this.theme = Blockly.Options.parseThemeOptions_(options); + /** @type {!Object} */ this.keyMap = keyMap; + /** @type {string} */ this.renderer = renderer; + /** @type {?Object} */ this.rendererOverrides = options['rendererOverrides']; /** * The SVG element for the grid pattern. * Created during injection. - * @type {!SVGElement} + * @type {SVGElement} */ - this.gridPattern = undefined; + this.gridPattern = null; /** * The parent of the current workspace, or null if there is no parent @@ -146,6 +178,12 @@ Blockly.Options = function(options) { * @type {Blockly.Workspace} */ this.parentWorkspace = options['parentWorkspace']; + + /** + * Map of plugin type to name of registered plugin or plugin class. + * @type {!Object.} + */ + this.plugins = plugins; }; /** @@ -158,15 +196,15 @@ Blockly.BlocklyOptions = function() {}; /** * If set, sets the translation of the workspace to match the scrollbars. - * @param {!Object} xyRatio Contains an x and/or y property which is a float - * between 0 and 1 specifying the degree of scrolling. + * @param {!{x:number,y:number}} xyRatio Contains an x and/or y property which + * is a float between 0 and 1 specifying the degree of scrolling. * @return {void} */ Blockly.Options.prototype.setMetrics; /** * Return an object with the metrics required to size the workspace. - * @return {!Object} Contains size and position metrics. + * @return {!Blockly.utils.Metrics} Contains size and position metrics. */ Blockly.Options.prototype.getMetrics; @@ -280,15 +318,20 @@ Blockly.Options.parseGridOptions_ = function(options) { */ Blockly.Options.parseThemeOptions_ = function(options) { var theme = options['theme'] || Blockly.Themes.Classic; - if (theme instanceof Blockly.Theme) { + if (typeof theme == 'string') { + return /** @type {!Blockly.Theme} */ ( + Blockly.registry.getObject(Blockly.registry.Type.THEME, theme)); + } else if (theme instanceof Blockly.Theme) { return /** @type {!Blockly.Theme} */ (theme); } - return Blockly.Theme.defineTheme(theme.name || 'builtin', theme); + return Blockly.Theme.defineTheme(theme.name || + ('builtin' + Blockly.utils.IdGenerator.getNextUniqueId()), theme); }; /** * Parse the provided toolbox tree into a consistent DOM format. - * @param {Node|string} tree DOM tree of blocks, or text representation of same. + * @param {Node|NodeList|?string} tree DOM tree of blocks, or text representation + * of same. * @return {Node} DOM tree of blocks, or null. */ Blockly.Options.parseToolboxTree = function(tree) { diff --git a/core/procedures.js b/core/procedures.js index 79fa4e630..610c7890d 100644 --- a/core/procedures.js +++ b/core/procedures.js @@ -60,23 +60,13 @@ Blockly.Procedures.ProcedureBlock; * list, and return value boolean. */ Blockly.Procedures.allProcedures = function(root) { - var blocks = root.getAllBlocks(false); - var proceduresReturn = []; - var proceduresNoReturn = []; - for (var i = 0; i < blocks.length; i++) { - if (blocks[i].getProcedureDef) { - var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( - blocks[i]); - var tuple = procedureBlock.getProcedureDef(); - if (tuple) { - if (tuple[2]) { - proceduresReturn.push(tuple); - } else { - proceduresNoReturn.push(tuple); - } - } - } - } + var proceduresNoReturn = root.getBlocksByType('procedures_defnoreturn', false) + .map(function(block) { + return /** @type {!Blockly.Procedures.ProcedureBlock} */ (block).getProcedureDef(); + }); + var proceduresReturn = root.getBlocksByType('procedures_defreturn', false).map(function(block) { + return /** @type {!Blockly.Procedures.ProcedureBlock} */ (block).getProcedureDef(); + }); proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_); proceduresReturn.sort(Blockly.Procedures.procTupleComparator_); return [proceduresNoReturn, proceduresReturn]; diff --git a/core/registry.js b/core/registry.js new file mode 100644 index 000000000..08f9dc037 --- /dev/null +++ b/core/registry.js @@ -0,0 +1,223 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview This file is a universal registry that provides generic methods + * for registering and unregistering different types of classes. + * @author aschmiedt@google.com (Abby Schmiedt) + */ +'use strict'; + +goog.provide('Blockly.registry'); + +goog.requireType('Blockly.blockRendering.Renderer'); +goog.requireType('Blockly.Field'); +goog.requireType('Blockly.IToolbox'); +goog.requireType('Blockly.Theme'); +goog.requireType('Blockly.utils.toolbox'); + + +/** + * A map of maps. With the keys being the type and name of the class we are + * registering and the value being the constructor function. + * e.g. {'field': {'field_angle': Blockly.FieldAngle}} + * + * @type {Object>} + */ +Blockly.registry.typeMap_ = {}; + +/** + * The string used to register the default class for a type of plugin. + * @type {string} + */ +Blockly.registry.DEFAULT = 'default'; + +/** + * A name with the type of the element stored in the generic. + * @param {string} name The name of the registry type. + * @constructor + * @template T + */ +Blockly.registry.Type = function(name) { + /** + * @type {string} + * @private + */ + this.name_ = name; +}; + +/** + * Returns the name of the type. + * @return {string} The name. + * @override + */ +Blockly.registry.Type.prototype.toString = function() { + return this.name_; +}; + +/** @type {!Blockly.registry.Type} */ +Blockly.registry.Type.RENDERER = new Blockly.registry.Type('renderer'); + +/** @type {!Blockly.registry.Type} */ +Blockly.registry.Type.FIELD = new Blockly.registry.Type('field'); + +/** @type {!Blockly.registry.Type} */ +Blockly.registry.Type.TOOLBOX = new Blockly.registry.Type('toolbox'); + +/** @type {!Blockly.registry.Type} */ +Blockly.registry.Type.THEME = new Blockly.registry.Type('theme'); + +/** + * Registers a class based on a type and name. + * @param {string|Blockly.registry.Type} type The type of the plugin. + * (e.g. Field, Renderer) + * @param {string} name The plugin's name. (Ex. field_angle, geras) + * @param {?function(new:T, ...?)|Object} registryItem The class or object to + * register. + * @throws {Error} if the type or name is empty, a name with the given type has + * already been registered, or if the given class or object is not valid for it's type. + * @template T + */ +Blockly.registry.register = function(type, name, registryItem) { + if ((!(type instanceof Blockly.registry.Type) && typeof type != 'string') || String(type).trim() == '') { + throw Error('Invalid type "' + type + '". The type must be a' + + ' non-empty string or a Blockly.registry.Type.'); + } + type = String(type).toLowerCase(); + + if ((typeof name != 'string') || (name.trim() == '')) { + throw Error('Invalid name "' + name + '". The name must be a' + + ' non-empty string.'); + } + name = name.toLowerCase(); + if (!registryItem) { + throw Error('Can not register a null value'); + } + var typeRegistry = Blockly.registry.typeMap_[type]; + // If the type registry has not been created, create it. + if (!typeRegistry) { + typeRegistry = Blockly.registry.typeMap_[type] = {}; + } + + // Validate that the given class has all the required properties. + Blockly.registry.validate_(type, registryItem); + + // If the name already exists throw an error. + if (typeRegistry[name]) { + throw Error('Name "' + name + '" with type "' + type + '" already registered.'); + } + typeRegistry[name] = registryItem; +}; + +/** + * Checks the given registry item for properties that are required based on the + * type. + * @param {string} type The type of the plugin. (e.g. Field, Renderer) + * @param {Function|Object} registryItem A class or object that we are checking + * for the required properties. + * @private + */ +Blockly.registry.validate_ = function(type, registryItem) { + switch (type) { + case String(Blockly.registry.Type.FIELD): + if (typeof registryItem.fromJson != 'function') { + throw Error('Type "' + type + '" must have a fromJson function'); + } + break; + } +}; + +/** + * Unregisters the registry item with the given type and name. + * @param {string|Blockly.registry.Type} type The type of the plugin. + * (e.g. Field, Renderer) + * @param {string} name The plugin's name. (Ex. field_angle, geras) + * @template T + */ +Blockly.registry.unregister = function(type, name) { + type = String(type).toLowerCase(); + name = name.toLowerCase(); + var typeRegistry = Blockly.registry.typeMap_[type]; + if (!typeRegistry) { + console.warn('No type "' + type + '" found'); + return; + } + if (!typeRegistry[name]) { + console.warn('No name "' + name + '" with type "' + type + '" found'); + return; + } + delete Blockly.registry.typeMap_[type][name]; +}; + +/** + * Gets the registry item for the given name and type. This can be either a + * class or an object.l + * @param {string|Blockly.registry.Type} type The type of the plugin. + * (e.g. Field, Renderer) + * @param {string} name The plugin's name. (Ex. field_angle, geras) + * @return {?function(new:T, ...?)|Object} The class or object with the given + * name and type or null if none exists. + * @template T + */ +Blockly.registry.getItem_ = function(type, name) { + type = String(type).toLowerCase(); + name = name.toLowerCase(); + var typeRegistry = Blockly.registry.typeMap_[type]; + if (!typeRegistry) { + console.warn('No type "' + type + '" found'); + return null; + } + if (!typeRegistry[name]) { + console.warn('No name "' + name + '" with type "' + type + '" found'); + return null; + } + return typeRegistry[name]; +}; + +/** + * Gets the class for the given name and type. + * @param {string|Blockly.registry.Type} type The type of the plugin. + * (e.g. Field, Renderer) + * @param {string} name The plugin's name. (Ex. field_angle, geras) + * @return {?function(new:T, ...?)} The class with the given name and type or + * null if none exists. + * @template T + */ +Blockly.registry.getClass = function(type, name) { + return /** @type {?function(new:T, ...?)} */ (Blockly.registry.getItem_(type, name)); +}; + +/** + * Gets the object for the given name and type. + * @param {string|Blockly.registry.Type} type The type of the plugin. + * (e.g. Category) + * @param {string} name The plugin's name. (Ex. logic_category) + * @returns {T} The object with the given name and type or null if none exists. + * @template T + */ +Blockly.registry.getObject = function(type, name) { + return /** @type {T} */ (Blockly.registry.getItem_(type, name)); +}; + +/** + * Gets the class from Blockly options for the given type. + * This is used for plugins that override a built in feature. (e.g. Toolbox) + * @param {Blockly.registry.Type} type The type of the plugin. + * @param {!Blockly.Options} options The option object to check for the given + * plugin. + * @return {?function(new:T, ...?)} The class for the plugin. + * @template T + */ +Blockly.registry.getClassFromOptions = function(type, options) { + var typeName = type.toString(); + var plugin = options.plugins[typeName] || Blockly.registry.DEFAULT; + + // If the user passed in a plugin class instead of a registered plugin name. + if (typeof plugin == 'function') { + return plugin; + } + return Blockly.registry.getClass(type, plugin); +}; diff --git a/core/rendered_connection.js b/core/rendered_connection.js index d6eab4827..811a0fe6b 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -59,6 +59,12 @@ Blockly.RenderedConnection = function(source, type) { * @private */ this.trackedState_ = Blockly.RenderedConnection.TrackedState.WILL_TRACK; + + /** + * Connection this connection connects to. Null if not connected. + * @type {Blockly.RenderedConnection} + */ + this.targetConnection = null; }; Blockly.utils.object.inherits(Blockly.RenderedConnection, Blockly.Connection); @@ -454,6 +460,8 @@ Blockly.RenderedConnection.prototype.disconnectInternal_ = function(parentBlock, if (childBlock.rendered) { childBlock.updateDisabled(); childBlock.render(); + // Reset visibility, since the child is now a top block. + childBlock.getSvgRoot().style.display = 'block'; } }; @@ -504,14 +512,16 @@ Blockly.RenderedConnection.prototype.connect_ = function(childConnection) { var parentConnection = this; var parentBlock = parentConnection.getSourceBlock(); var childBlock = childConnection.getSourceBlock(); + var parentRendered = parentBlock.rendered; + var childRendered = childBlock.rendered; - if (parentBlock.rendered) { + if (parentRendered) { parentBlock.updateDisabled(); } - if (childBlock.rendered) { + if (childRendered) { childBlock.updateDisabled(); } - if (parentBlock.rendered && childBlock.rendered) { + if (parentRendered && childRendered) { if (parentConnection.type == Blockly.NEXT_STATEMENT || parentConnection.type == Blockly.PREVIOUS_STATEMENT) { // Child block may need to square off its corners if it is in a stack. @@ -523,6 +533,13 @@ Blockly.RenderedConnection.prototype.connect_ = function(childConnection) { parentBlock.render(); } } + + // The input the child block is connected to (if any). + var parentInput = parentBlock.getInputWithBlock(childBlock); + if (parentInput) { + var visible = parentInput.isVisible(); + childBlock.getSvgRoot().style.display = visible ? 'block' : 'none'; + } }; /** diff --git a/core/renderers/common/block_rendering.js b/core/renderers/common/block_rendering.js index 8a7757cd1..8b4a2b572 100644 --- a/core/renderers/common/block_rendering.js +++ b/core/renderers/common/block_rendering.js @@ -16,16 +16,10 @@ */ goog.provide('Blockly.blockRendering'); +goog.require('Blockly.registry'); goog.require('Blockly.utils.object'); -/** - * The set of all registered renderers, keyed by their name. - * @type {!Object} - * @private - */ -Blockly.blockRendering.rendererMap_ = {}; - /** * Whether or not the debugger is turned on. * @type {boolean} @@ -41,10 +35,8 @@ Blockly.blockRendering.useDebugger = false; * @throws {Error} if a renderer with the same name has already been registered. */ Blockly.blockRendering.register = function(name, rendererClass) { - if (Blockly.blockRendering.rendererMap_[name]) { - throw Error('Renderer has already been registered.'); - } - Blockly.blockRendering.rendererMap_[name] = rendererClass; + Blockly.registry.register(Blockly.registry.Type.RENDERER, name, + rendererClass); }; /** @@ -52,14 +44,8 @@ Blockly.blockRendering.register = function(name, rendererClass) { * @param {string} name The name of the renderer. */ Blockly.blockRendering.unregister = function(name) { - if (Blockly.blockRendering.rendererMap_[name]) { - delete Blockly.blockRendering.rendererMap_[name]; - } else { - console.warn('No renderer mapping for name "' + name + - '" found to unregister'); - } + Blockly.registry.unregister(Blockly.registry.Type.RENDERER, name); }; - /** * Turn on the blocks debugger. * @package @@ -85,12 +71,11 @@ Blockly.blockRendering.stopDebugger = function() { * Already initialized. * @package */ + Blockly.blockRendering.init = function(name, theme, opt_rendererOverrides) { - if (!Blockly.blockRendering.rendererMap_[name]) { - throw Error('Renderer not registered: ', name); - } - var renderer = (/** @type {!Blockly.blockRendering.Renderer} */ ( - new Blockly.blockRendering.rendererMap_[name](name))); + var rendererClass = Blockly.registry.getClass( + Blockly.registry.Type.RENDERER, name); + var renderer = new rendererClass(name); renderer.init(theme, opt_rendererOverrides); return renderer; }; diff --git a/core/renderers/common/constants.js b/core/renderers/common/constants.js index 8a5b19cff..434e5e3cd 100644 --- a/core/renderers/common/constants.js +++ b/core/renderers/common/constants.js @@ -256,7 +256,7 @@ Blockly.blockRendering.ConstantProvider = function() { * @type {number} */ this.FIELD_TEXT_HEIGHT = -1; // Dynamically set - + /** * Text baseline. This constant is dynamically set in ``setFontConstants_`` * to be the baseline of the text based on the font used. @@ -1168,9 +1168,8 @@ Blockly.blockRendering.ConstantProvider.prototype.getCSS_ = function(selector) { // Text. selector + ' .blocklyText, ', selector + ' .blocklyFlyoutLabelText {', - 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', - 'font-size: ' + this.FIELD_TEXT_FONTSIZE + 'pt;', - 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', + 'font: ' + this.FIELD_TEXT_FONTWEIGHT + ' ' + + this.FIELD_TEXT_FONTSIZE + 'pt ' + this.FIELD_TEXT_FONTFAMILY + ';', '}', // Fields. @@ -1233,7 +1232,7 @@ Blockly.blockRendering.ConstantProvider.prototype.getCSS_ = function(selector) { // Insertion marker. selector + ' .blocklyInsertionMarker>.blocklyPath {', 'fill-opacity: ' + this.INSERTION_MARKER_OPACITY + ';', - 'stroke: none', + 'stroke: none;', '}', /* eslint-enable indent */ ]; diff --git a/core/renderers/common/i_path_object.js b/core/renderers/common/i_path_object.js index 82a58f513..8b18426cf 100644 --- a/core/renderers/common/i_path_object.js +++ b/core/renderers/common/i_path_object.js @@ -27,6 +27,38 @@ goog.requireType('Blockly.Theme'); */ Blockly.blockRendering.IPathObject = function(_root, _constants) {}; +/** + * The primary path of the block. + * @type {!SVGElement} + */ +Blockly.blockRendering.IPathObject.prototype.svgPath; + +/** + * The renderer's constant provider. + * @type {!Blockly.blockRendering.ConstantProvider} + */ +Blockly.blockRendering.IPathObject.prototype.constants; + +/** + * The primary path of the block. + * @type {!Blockly.Theme.BlockStyle} + */ +Blockly.blockRendering.IPathObject.prototype.style; + +/** + * Holds the cursors svg element when the cursor is attached to the block. + * This is null if there is no cursor on the block. + * @type {SVGElement} + */ +Blockly.blockRendering.IPathObject.prototype.cursorSvg; + +/** + * Holds the markers svg element when the marker is attached to the block. + * This is null if there is no marker on the block. + * @type {SVGElement} + */ +Blockly.blockRendering.IPathObject.prototype.markerSvg; + /** * Set the path generated by the renderer onto the respective SVG element. * @param {string} pathString The path. diff --git a/core/renderers/common/info.js b/core/renderers/common/info.js index a49624316..e58fc842d 100644 --- a/core/renderers/common/info.js +++ b/core/renderers/common/info.js @@ -581,7 +581,7 @@ Blockly.blockRendering.RenderInfo.prototype.addAlignmentPadding_ = function(row, if (row.hasExternalInput || row.hasStatement) { row.widthWithConnectedBlocks += missingSpace; } - + // Decide where the extra padding goes. if (row.align == Blockly.ALIGN_LEFT) { // Add padding to the end of the row. diff --git a/core/renderers/common/marker_svg.js b/core/renderers/common/marker_svg.js index a9b005b88..2fda38c66 100644 --- a/core/renderers/common/marker_svg.js +++ b/core/renderers/common/marker_svg.js @@ -42,7 +42,7 @@ Blockly.blockRendering.MarkerSvg = function(workspace, constants, marker) { /** * The workspace, field, or block that the marker SVG element should be * attached to. - * @type {Blockly.WorkspaceSvg|Blockly.Field|Blockly.BlockSvg} + * @type {Blockly.IASTNodeLocationSvg} * @private */ this.parent_ = null; @@ -85,8 +85,7 @@ Blockly.blockRendering.MarkerSvg.MARKER_CLASS = 'blocklyMarker'; /** * What we multiply the height by to get the height of the marker. * Only used for the block and block connections. - * @type {number} - * @const + * @const {number} */ Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER = 3 / 4; @@ -98,6 +97,14 @@ Blockly.blockRendering.MarkerSvg.prototype.getSvgRoot = function() { return this.svgGroup_; }; +/** + * Get the marker. + * @return {!Blockly.Marker} The marker to draw for. + */ +Blockly.blockRendering.MarkerSvg.prototype.getMarker = function() { + return this.marker_; +}; + /** * True if the marker should be drawn as a cursor, false otherwise. * A cursor is drawn as a flashing line. A marker is drawn as a solid line. @@ -123,15 +130,13 @@ Blockly.blockRendering.MarkerSvg.prototype.createDom = function() { }, null); this.createDomInternal_(); - this.applyColour_(); return this.svgGroup_; }; /** * Attaches the SVG root of the marker to the SVG group of the parent. - * @param {!Blockly.WorkspaceSvg|!Blockly.Field|!Blockly.BlockSvg} newParent - * The workspace, field, or block that the marker SVG element should be - * attached to. + * @param {!Blockly.IASTNodeLocationSvg} newParent The workspace, field, or + * block that the marker SVG element should be attached to. * @protected */ Blockly.blockRendering.MarkerSvg.prototype.setParent_ = function(newParent) { @@ -149,6 +154,63 @@ Blockly.blockRendering.MarkerSvg.prototype.setParent_ = function(newParent) { this.parent_ = newParent; }; +/** + * Update the marker. + * @param {Blockly.ASTNode} oldNode The previous node the marker was on or null. + * @param {Blockly.ASTNode} curNode The node that we want to draw the marker for. + */ +Blockly.blockRendering.MarkerSvg.prototype.draw = function(oldNode, curNode) { + if (!curNode) { + this.hide(); + return; + } + + this.constants_ = this.workspace_.getRenderer().getConstants(); + + var defaultColour = this.isCursor() ? this.constants_.CURSOR_COLOUR : + this.constants_.MARKER_COLOUR; + this.colour_ = this.marker_.colour || defaultColour; + this.applyColour_(curNode); + + this.showAtLocation_(curNode); + + this.fireMarkerEvent_(oldNode, curNode); + + // Ensures the marker will be visible immediately after the move. + var animate = this.currentMarkerSvg.childNodes[0]; + if (animate !== undefined) { + animate.beginElement && animate.beginElement(); + } +}; + + +/** + * Update the marker's visible state based on the type of curNode.. + * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showAtLocation_ = function(curNode) { + var curNodeAsConnection = + /** @type {!Blockly.Connection} */ (curNode.getLocation()); + if (curNode.getType() == Blockly.ASTNode.types.BLOCK) { + this.showWithBlock_(curNode); + } else if (curNode.getType() == Blockly.ASTNode.types.OUTPUT) { + this.showWithOutput_(curNode); + } else if (curNodeAsConnection.type == Blockly.INPUT_VALUE) { + this.showWithInput_(curNode); + } else if (curNodeAsConnection.type == Blockly.NEXT_STATEMENT) { + this.showWithNext_(curNode); + } else if (curNode.getType() == Blockly.ASTNode.types.PREVIOUS) { + this.showWithPrevious_(curNode); + } else if (curNode.getType() == Blockly.ASTNode.types.FIELD) { + this.showWithField_(curNode); + } else if (curNode.getType() == Blockly.ASTNode.types.WORKSPACE) { + this.showWithCoordinates_(curNode); + } else if (curNode.getType() == Blockly.ASTNode.types.STACK) { + this.showWithStack_(curNode); + } +}; + /************************** * Display **************************/ @@ -156,13 +218,12 @@ Blockly.blockRendering.MarkerSvg.prototype.setParent_ = function(newParent) { /** * Show the marker as a combination of the previous connection and block, * the output connection and block, or just the block. - * @param {Blockly.BlockSvg} block The block the marker is currently on. - * @protected + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. + * @private */ -Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_ = function(block) { - if (!block) { - return; - } +Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_ = function( + curNode) { + var block = /** @type {!Blockly.BlockSvg} */ (curNode.getSourceBlock()); var width = block.width; var height = block.height; var markerHeight = height * Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER; @@ -177,18 +238,46 @@ Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_ = function(b } else { this.positionBlock_(width, markerOffset, markerHeight); } - this.setParent_(block); this.showCurrent_(); }; /** - * Show the visual representation of a workspace coordinate. - * This is a horizontal line. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * Position and display the marker for a block. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. * @protected */ -Blockly.blockRendering.MarkerSvg.prototype.showWithCoordinates_ = function(curNode) { +Blockly.blockRendering.MarkerSvg.prototype.showWithBlock_ = function(curNode) { + this.showWithBlockPrevOutput_(curNode); +}; + +/** + * Position and display the marker for a previous connection. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithPrevious_ = function( + curNode) { + this.showWithBlockPrevOutput_(curNode); +}; + +/** + * Position and display the marker for an output connection. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithOutput_ = function(curNode) { + this.showWithBlockPrevOutput_(curNode); +}; + +/** + * Position and display the marker for a workspace coordinate. + * This is a horizontal line. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithCoordinates_ = function( + curNode) { var wsCoordinate = curNode.getWsCoordinate(); var x = wsCoordinate.x; var y = wsCoordinate.y; @@ -203,9 +292,9 @@ Blockly.blockRendering.MarkerSvg.prototype.showWithCoordinates_ = function(curNo }; /** - * Show the visual representation of a field. + * Position and display the marker for a field. * This is a box around the field. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. * @protected */ Blockly.blockRendering.MarkerSvg.prototype.showWithField_ = function(curNode) { @@ -219,9 +308,9 @@ Blockly.blockRendering.MarkerSvg.prototype.showWithField_ = function(curNode) { }; /** - * Show the visual representation of an input. + * Position and display the marker for an input. * This is a puzzle piece. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. * @protected */ Blockly.blockRendering.MarkerSvg.prototype.showWithInput_ = function(curNode) { @@ -236,14 +325,16 @@ Blockly.blockRendering.MarkerSvg.prototype.showWithInput_ = function(curNode) { /** - * Show the visual representation of a next connection. + * Position and display the marker for a next connection. * This is a horizontal line. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. * @protected */ Blockly.blockRendering.MarkerSvg.prototype.showWithNext_ = function(curNode) { - var connection = curNode.getLocation(); - var targetBlock = /** @type {Blockly.BlockSvg} */ (connection.getSourceBlock()); + var connection = + /** @type {!Blockly.RenderedConnection} */ (curNode.getLocation()); + var targetBlock = + /** @type {Blockly.BlockSvg} */ (connection.getSourceBlock()); var x = 0; var y = connection.getOffsetInBlock().y; var width = targetBlock.getHeightWidth().width; @@ -256,9 +347,9 @@ Blockly.blockRendering.MarkerSvg.prototype.showWithNext_ = function(curNode) { }; /** - * Show the visual representation of a stack. + * Position and display the marker for a stack. * This is a box with extra padding around the entire stack of blocks. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. * @protected */ Blockly.blockRendering.MarkerSvg.prototype.showWithStack_ = function(curNode) { @@ -305,7 +396,7 @@ Blockly.blockRendering.MarkerSvg.prototype.showCurrent_ = function() { * @param {number} width The width of the block. * @param {number} markerOffset The extra padding for around the block. * @param {number} markerHeight The height of the marker. - * @private + * @protected */ Blockly.blockRendering.MarkerSvg.prototype.positionBlock_ = function( width, markerOffset, markerHeight) { @@ -323,10 +414,12 @@ Blockly.blockRendering.MarkerSvg.prototype.positionBlock_ = function( /** * Position the marker for an input connection. * Displays a filled in puzzle piece. - * @param {!Blockly.RenderedConnection} connection The connection to position marker around. - * @private + * @param {!Blockly.RenderedConnection} connection The connection to position + * marker around. + * @protected */ -Blockly.blockRendering.MarkerSvg.prototype.positionInput_ = function(connection) { +Blockly.blockRendering.MarkerSvg.prototype.positionInput_ = function( + connection) { var x = connection.getOffsetInBlock().x; var y = connection.getOffsetInBlock().y; @@ -335,7 +428,8 @@ Blockly.blockRendering.MarkerSvg.prototype.positionInput_ = function(connection) this.markerInput_.setAttribute('d', path); this.markerInput_.setAttribute('transform', - 'translate(' + x + ',' + y + ')' + (this.workspace_.RTL ? ' scale(-1 1)' : '')); + 'translate(' + x + ',' + y + ')' + + (this.workspace_.RTL ? ' scale(-1 1)' : '')); this.currentMarkerSvg = this.markerInput_; }; @@ -347,7 +441,8 @@ Blockly.blockRendering.MarkerSvg.prototype.positionInput_ = function(connection) * @param {number} width The new width, in workspace units. * @protected */ -Blockly.blockRendering.MarkerSvg.prototype.positionLine_ = function(x, y, width) { +Blockly.blockRendering.MarkerSvg.prototype.positionLine_ = function( + x, y, width) { this.markerSvgLine_.setAttribute('x', x); this.markerSvgLine_.setAttribute('y', y); this.markerSvgLine_.setAttribute('width', width); @@ -360,7 +455,7 @@ Blockly.blockRendering.MarkerSvg.prototype.positionLine_ = function(x, y, width) * @param {number} width The width of the block. * @param {number} height The height of the block. * @param {!Object} connectionShape The shape object for the connection. - * @private + * @protected */ Blockly.blockRendering.MarkerSvg.prototype.positionOutput_ = function( width, height, connectionShape) { @@ -387,7 +482,7 @@ Blockly.blockRendering.MarkerSvg.prototype.positionOutput_ = function( * @param {number} markerOffset The offset of the marker from around the block. * @param {number} markerHeight The height of the marker. * @param {!Object} connectionShape The shape object for the connection. - * @private + * @protected */ Blockly.blockRendering.MarkerSvg.prototype.positionPrevious_ = function( width, markerOffset, markerHeight, connectionShape) { @@ -415,7 +510,8 @@ Blockly.blockRendering.MarkerSvg.prototype.positionPrevious_ = function( * @param {number} height The new height, in workspace units. * @protected */ -Blockly.blockRendering.MarkerSvg.prototype.positionRect_ = function(x, y, width, height) { +Blockly.blockRendering.MarkerSvg.prototype.positionRect_ = function( + x, y, width, height) { this.markerSvgRect_.setAttribute('x', x); this.markerSvgRect_.setAttribute('y', y); this.markerSvgRect_.setAttribute('width', width); @@ -434,7 +530,6 @@ Blockly.blockRendering.MarkerSvg.prototype.flipRtl_ = function(markerSvg) { /** * Hide the marker. - * @package */ Blockly.blockRendering.MarkerSvg.prototype.hide = function() { this.markerSvgLine_.style.display = 'none'; @@ -443,64 +538,6 @@ Blockly.blockRendering.MarkerSvg.prototype.hide = function() { this.markerBlock_.style.display = 'none'; }; -/** - * Update the marker. - * @param {Blockly.ASTNode} oldNode The previous node the marker was on or null. - * @param {Blockly.ASTNode} curNode The node that we want to draw the marker for. - * @package - */ -Blockly.blockRendering.MarkerSvg.prototype.draw = function(oldNode, curNode) { - if (!curNode) { - this.hide(); - return; - } - - this.constants_ = this.workspace_.getRenderer().getConstants(); - - var defaultColour = this.isCursor() ? this.constants_.CURSOR_COLOUR : - this.constants_.MARKER_COLOUR; - this.colour_ = this.marker_.colour || defaultColour; - this.applyColour_(); - - this.showAtLocation_(curNode); - - this.firemarkerEvent_(oldNode, curNode); - - // Ensures the marker will be visible immediately after the move. - var animate = this.currentMarkerSvg.childNodes[0]; - if (animate !== undefined) { - animate.beginElement && animate.beginElement(); - } -}; - - -/** - * Update the marker's visible state based on the type of curNode.. - * @param {Blockly.ASTNode} curNode The node that we want to draw the marker for. - * @protected - */ -Blockly.blockRendering.MarkerSvg.prototype.showAtLocation_ = function(curNode) { - if (curNode.getType() == Blockly.ASTNode.types.BLOCK) { - var block = /** @type {Blockly.BlockSvg} */ (curNode.getLocation()); - this.showWithBlockPrevOutput_(block); - } else if (curNode.getType() == Blockly.ASTNode.types.OUTPUT) { - var outputBlock = /** @type {Blockly.BlockSvg} */ (curNode.getLocation().getSourceBlock()); - this.showWithBlockPrevOutput_(outputBlock); - } else if (curNode.getLocation().type == Blockly.INPUT_VALUE) { - this.showWithInput_(curNode); - } else if (curNode.getLocation().type == Blockly.NEXT_STATEMENT) { - this.showWithNext_(curNode); - } else if (curNode.getType() == Blockly.ASTNode.types.PREVIOUS) { - var previousBlock = /** @type {Blockly.BlockSvg} */ (curNode.getLocation().getSourceBlock()); - this.showWithBlockPrevOutput_(previousBlock); - } else if (curNode.getType() == Blockly.ASTNode.types.FIELD) { - this.showWithField_(curNode); - } else if (curNode.getType() == Blockly.ASTNode.types.WORKSPACE) { - this.showWithCoordinates_(curNode); - } else if (curNode.getType() == Blockly.ASTNode.types.STACK) { - this.showWithStack_(curNode); - } -}; /** * Fire event for the marker or marker. @@ -508,12 +545,14 @@ Blockly.blockRendering.MarkerSvg.prototype.showAtLocation_ = function(curNode) { * @param {!Blockly.ASTNode} curNode The new node the marker is currently on. * @private */ -Blockly.blockRendering.MarkerSvg.prototype.firemarkerEvent_ = function(oldNode, curNode) { +Blockly.blockRendering.MarkerSvg.prototype.fireMarkerEvent_ = function( + oldNode, curNode) { var curBlock = curNode.getSourceBlock(); var eventType = this.isCursor() ? 'cursorMove' : 'markerMove'; var event = new Blockly.Events.Ui(curBlock, eventType, oldNode, curNode); if (curNode.getType() == Blockly.ASTNode.types.WORKSPACE) { - event.workspaceId = curNode.getLocation().id; + event.workspaceId = + (/** @type {!Blockly.Workspace} */ (curNode.getLocation())).id; } Blockly.Events.fire(event); }; @@ -555,7 +594,8 @@ Blockly.blockRendering.MarkerSvg.prototype.createDomInternal_ = function() { 'height': this.constants_.WS_CURSOR_HEIGHT }, this.svgGroup_); - // A horizontal line used to represent a workspace coordinate or next connection. + // A horizontal line used to represent a workspace coordinate or next + // connection. this.markerSvgLine_ = Blockly.utils.dom.createSvgElement('rect', { 'width': this.constants_.CURSOR_WS_WIDTH, @@ -609,9 +649,11 @@ Blockly.blockRendering.MarkerSvg.prototype.createDomInternal_ = function() { /** * Apply the marker's colour. + * @param {!Blockly.ASTNode} _curNode The node that we want to draw the marker + * for. * @protected */ -Blockly.blockRendering.MarkerSvg.prototype.applyColour_ = function() { +Blockly.blockRendering.MarkerSvg.prototype.applyColour_ = function(_curNode) { this.markerSvgLine_.setAttribute('fill', this.colour_); this.markerSvgRect_.setAttribute('stroke', this.colour_); this.markerInput_.setAttribute('fill', this.colour_); @@ -627,7 +669,6 @@ Blockly.blockRendering.MarkerSvg.prototype.applyColour_ = function() { /** * Dispose of this marker. - * @package */ Blockly.blockRendering.MarkerSvg.prototype.dispose = function() { if (this.svgGroup_) { diff --git a/core/renderers/common/path_object.js b/core/renderers/common/path_object.js index 73d1cc732..602f27448 100644 --- a/core/renderers/common/path_object.js +++ b/core/renderers/common/path_object.js @@ -43,7 +43,7 @@ Blockly.blockRendering.PathObject = function(root, style, constants) { /** * The primary path of the block. - * @type {SVGElement} + * @type {!SVGElement} * @package */ this.svgPath = Blockly.utils.dom.createSvgElement('path', diff --git a/core/renderers/common/renderer.js b/core/renderers/common/renderer.js index 74b9f6f3d..05885d18c 100644 --- a/core/renderers/common/renderer.js +++ b/core/renderers/common/renderer.js @@ -21,6 +21,7 @@ goog.require('Blockly.blockRendering.RenderInfo'); goog.require('Blockly.InsertionMarkerManager'); goog.requireType('Blockly.blockRendering.Debug'); +goog.requireType('Blockly.IRegistrable'); /** @@ -28,6 +29,7 @@ goog.requireType('Blockly.blockRendering.Debug'); * @param {string} name The renderer name. * @package * @constructor + * @implements {Blockly.IRegistrable} */ Blockly.blockRendering.Renderer = function(name) { diff --git a/core/renderers/geras/constants.js b/core/renderers/geras/constants.js index 0499a135b..ee7b96856 100644 --- a/core/renderers/geras/constants.js +++ b/core/renderers/geras/constants.js @@ -57,7 +57,7 @@ Blockly.geras.ConstantProvider.prototype.getCSS_ = function(selector) { selector + ' .blocklyInsertionMarker>.blocklyPathLight,', selector + ' .blocklyInsertionMarker>.blocklyPathDark {', 'fill-opacity: ' + this.INSERTION_MARKER_OPACITY + ';', - 'stroke: none', + 'stroke: none;', '}', /* eslint-enable indent */ ]); diff --git a/core/renderers/geras/path_object.js b/core/renderers/geras/path_object.js index 76884c9bc..dd572aab7 100644 --- a/core/renderers/geras/path_object.js +++ b/core/renderers/geras/path_object.js @@ -54,7 +54,7 @@ Blockly.geras.PathObject = function(root, style, constants) { /** * The primary path of the block. - * @type {SVGElement} + * @type {!SVGElement} * @package */ this.svgPath = Blockly.utils.dom.createSvgElement('path', @@ -122,7 +122,7 @@ Blockly.geras.PathObject.prototype.applyColour = function(block) { this.svgPathDark.setAttribute('fill', this.colourDark); Blockly.geras.PathObject.superClass_.applyColour.call(this, block); - + this.svgPath.setAttribute('stroke', 'none'); }; diff --git a/core/renderers/zelos/constants.js b/core/renderers/zelos/constants.js index 57ebdca11..acfe8c36e 100644 --- a/core/renderers/zelos/constants.js +++ b/core/renderers/zelos/constants.js @@ -69,7 +69,7 @@ Blockly.zelos.ConstantProvider = function() { * @override */ this.NOTCH_OFFSET_LEFT = 3 * this.GRID_UNIT; - + /** * @override */ @@ -259,7 +259,7 @@ Blockly.zelos.ConstantProvider = function() { * @override */ this.FIELD_BORDER_RECT_X_PADDING = 2 * this.GRID_UNIT; - + /** * @override */ @@ -905,13 +905,12 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) { return [ /* eslint-disable indent */ // Text. - selector + ' .blocklyText, ', + selector + ' .blocklyText,', selector + ' .blocklyFlyoutLabelText {', - 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', - 'font-size: ' + this.FIELD_TEXT_FONTSIZE + 'pt;', - 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', + 'font: ' + this.FIELD_TEXT_FONTWEIGHT + ' ' + + this.FIELD_TEXT_FONTSIZE + 'pt ' + this.FIELD_TEXT_FONTFAMILY + ';', '}', - + // Fields. selector + ' .blocklyText {', 'fill: #fff;', @@ -926,7 +925,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) { selector + ' .blocklyEditableText>g>text {', 'fill: #575E75;', '}', - + // Flyout labels. selector + ' .blocklyFlyoutLabelText {', 'fill: #575E75;', @@ -939,7 +938,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) { // Editable field hover. selector + ' .blocklyDraggable:not(.blocklyDisabled)', - ' .blocklyEditableText:not(.editing):hover>rect ,', + ' .blocklyEditableText:not(.editing):hover>rect,', selector + ' .blocklyDraggable:not(.blocklyDisabled)', ' .blocklyEditableText:not(.editing):hover>.blocklyPath {', 'stroke: #fff;', @@ -952,7 +951,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) { 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', 'color: #575E75;', '}', - + // Dropdown field. selector + ' .blocklyDropdownText {', 'fill: #fff !important;', @@ -979,7 +978,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) { // Insertion marker. selector + ' .blocklyInsertionMarker>.blocklyPath {', 'fill-opacity: ' + this.INSERTION_MARKER_OPACITY + ';', - 'stroke: none', + 'stroke: none;', '}', /* eslint-enable indent */ ]; diff --git a/core/renderers/zelos/marker_svg.js b/core/renderers/zelos/marker_svg.js index 7c9bdd120..0b5d31f12 100644 --- a/core/renderers/zelos/marker_svg.js +++ b/core/renderers/zelos/marker_svg.js @@ -32,11 +32,13 @@ Blockly.utils.object.inherits(Blockly.zelos.MarkerSvg, Blockly.blockRendering.MarkerSvg); /** - * @override + * Position and display the marker for an input or an output connection. + * @param {!Blockly.ASTNode} curNode The node to draw the marker for. + * @private */ -Blockly.zelos.MarkerSvg.prototype.showWithInput_ = function(curNode) { +Blockly.zelos.MarkerSvg.prototype.showWithInputOutput_ = function(curNode) { var block = /** @type {!Blockly.BlockSvg} */ (curNode.getSourceBlock()); - var connection = curNode.getLocation(); + var connection = /** @type {!Blockly.Connection} */ (curNode.getLocation()); var offsetInBlock = connection.getOffsetInBlock(); this.positionCircle_(offsetInBlock.x, offsetInBlock.y); @@ -44,6 +46,20 @@ Blockly.zelos.MarkerSvg.prototype.showWithInput_ = function(curNode) { this.showCurrent_(); }; +/** + * @override + */ +Blockly.zelos.MarkerSvg.prototype.showWithOutput_ = function(curNode) { + this.showWithInputOutput_(curNode); +}; + +/** + * @override + */ +Blockly.zelos.MarkerSvg.prototype.showWithInput_ = function(curNode) { + this.showWithInputOutput_(curNode); +}; + /** * Draw a rectangle around the block. * @param {!Blockly.ASTNode} curNode The current node of the marker. @@ -72,25 +88,6 @@ Blockly.zelos.MarkerSvg.prototype.positionCircle_ = function(x, y) { this.currentMarkerSvg = this.markerCircle_; }; -/** - * @override - */ -Blockly.zelos.MarkerSvg.prototype.showAtLocation_ = function(curNode) { - var handled = false; - if (curNode.getType() == Blockly.ASTNode.types.OUTPUT) { - // Inputs and outputs are drawn the same. - this.showWithInput_(curNode); - handled = true; - } else if (curNode.getType() == Blockly.ASTNode.types.BLOCK) { - this.showWithBlock_(curNode); - handled = true; - } - - if (!handled) { - Blockly.zelos.MarkerSvg.superClass_.showAtLocation_.call(this, curNode); - } -}; - /** * @override */ @@ -134,8 +131,8 @@ Blockly.zelos.MarkerSvg.prototype.createDomInternal_ = function() { /** * @override */ -Blockly.zelos.MarkerSvg.prototype.applyColour_ = function() { - Blockly.zelos.MarkerSvg.superClass_.applyColour_.call(this); +Blockly.zelos.MarkerSvg.prototype.applyColour_ = function(curNode) { + Blockly.zelos.MarkerSvg.superClass_.applyColour_.call(this, curNode); this.markerCircle_.setAttribute('fill', this.colour_); this.markerCircle_.setAttribute('stroke', this.colour_); diff --git a/core/requires.js b/core/requires.js index a9c87b5f6..d7008717f 100644 --- a/core/requires.js +++ b/core/requires.js @@ -63,13 +63,6 @@ goog.require('Blockly.FieldMultilineInput'); goog.require('Blockly.FieldNumber'); goog.require('Blockly.FieldVariable'); -// If you'd like to include the date field in your build, you will also need to -// include the Closure library as a build dependency. You can do so by running: -// gulp build-compressed --closure-library -// Be sure to also include "google-closure-library" to your list of -// devDependencies. -// goog.require('Blockly.FieldDate'); - // Blockly Renderers. // At least one renderer is mandatory. Geras is the default one. diff --git a/core/scrollbar.js b/core/scrollbar.js index eaa522c5b..753e219de 100644 --- a/core/scrollbar.js +++ b/core/scrollbar.js @@ -17,6 +17,7 @@ goog.require('Blockly.Touch'); goog.require('Blockly.utils'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.Metrics'); /** @@ -26,7 +27,7 @@ goog.require('Blockly.utils.dom'); /** * Class for a pair of scrollbars. Horizontal and vertical. - * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbars to. + * @param {!Blockly.WorkspaceSvg} workspace Workspace to bind the scrollbars to. * @constructor */ Blockly.ScrollbarPair = function(workspace) { @@ -44,14 +45,14 @@ Blockly.ScrollbarPair = function(workspace) { }, null); Blockly.utils.dom.insertAfter(this.corner_, workspace.getBubbleCanvas()); -}; -/** - * Previously recorded metrics from the workspace. - * @type {Object} - * @private - */ -Blockly.ScrollbarPair.prototype.oldHostMetrics_ = null; + /** + * Previously recorded metrics from the workspace. + * @type {?Blockly.utils.Metrics} + * @private + */ + this.oldHostMetrics_ = null; +}; /** * Dispose of this pair of scrollbars. @@ -117,12 +118,12 @@ Blockly.ScrollbarPair.prototype.resize = function() { if (!this.oldHostMetrics_ || this.oldHostMetrics_.viewWidth != hostMetrics.viewWidth || this.oldHostMetrics_.absoluteLeft != hostMetrics.absoluteLeft) { - this.corner_.setAttribute('x', this.vScroll.position_.x); + this.corner_.setAttribute('x', this.vScroll.position.x); } if (!this.oldHostMetrics_ || this.oldHostMetrics_.viewHeight != hostMetrics.viewHeight || this.oldHostMetrics_.absoluteTop != hostMetrics.absoluteTop) { - this.corner_.setAttribute('y', this.hScroll.position_.y); + this.corner_.setAttribute('y', this.hScroll.position.y); } // Cache the current metrics to potentially short-cut the next resize event. @@ -144,8 +145,8 @@ Blockly.ScrollbarPair.prototype.set = function(x, y) { // Combining them speeds up rendering. var xyRatio = {}; - var hHandlePosition = x * this.hScroll.ratio_; - var vHandlePosition = y * this.vScroll.ratio_; + var hHandlePosition = x * this.hScroll.ratio; + var vHandlePosition = y * this.vScroll.ratio; var hBarLength = this.hScroll.scrollViewSize_; var vBarLength = this.vScroll.scrollViewSize_; @@ -179,7 +180,7 @@ Blockly.ScrollbarPair.prototype.getRatio_ = function(handlePosition, viewSize) { * Class for a pure SVG scrollbar. * This technique offers a scrollbar that is guaranteed to work, but may not * look or behave like the system's scrollbars. - * @param {!Blockly.Workspace} workspace Workspace to bind the scrollbar to. + * @param {!Blockly.WorkspaceSvg} workspace Workspace to bind the scrollbar to. * @param {boolean} horizontal True if horizontal, false if vertical. * @param {boolean=} opt_pair True if scrollbar is part of a horiz/vert pair. * @param {string=} opt_class A class to be applied to this scrollbar. @@ -191,6 +192,12 @@ Blockly.Scrollbar = function(workspace, horizontal, opt_pair, opt_class) { this.horizontal_ = horizontal; this.oldHostMetrics_ = null; + /** + * @type {?number} + * @package + */ + this.ratio = null; + this.createDom_(opt_class); /** @@ -198,9 +205,9 @@ Blockly.Scrollbar = function(workspace, horizontal, opt_pair, opt_class) { * to the scrollbar's origin. This is usually relative to the injection div * origin. * @type {Blockly.utils.Coordinate} - * @private + * @package */ - this.position_ = new Blockly.utils.Coordinate(0, 0); + this.position = new Blockly.utils.Coordinate(0, 0); // Store the thickness in a temp variable for readability. var scrollbarThickness = Blockly.Scrollbar.scrollbarThickness; @@ -295,10 +302,10 @@ if (Blockly.Touch.TOUCH_ENABLED) { } /** - * @param {Object} first An object containing computed measurements of a - * workspace. - * @param {Object} second Another object containing computed measurements of a - * workspace. + * @param {Blockly.utils.Metrics} first An object containing computed + * measurements of a workspace. + * @param {Blockly.utils.Metrics} second Another object containing computed + * measurements of a workspace. * @return {boolean} Whether the two sets of metrics are equivalent. * @private */ @@ -392,23 +399,23 @@ Blockly.ScrollbarPair.prototype.setContainerVisible = function(visible) { * scrollbar's origin. This sets the scrollbar's location within the workspace. * @param {number} x The new x coordinate. * @param {number} y The new y coordinate. - * @private + * @package */ -Blockly.Scrollbar.prototype.setPosition_ = function(x, y) { - this.position_.x = x; - this.position_.y = y; +Blockly.Scrollbar.prototype.setPosition = function(x, y) { + this.position.x = x; + this.position.y = y; - var tempX = this.position_.x + this.origin_.x; - var tempY = this.position_.y + this.origin_.y; + var tempX = this.position.x + this.origin_.x; + var tempY = this.position.y + this.origin_.y; var transform = 'translate(' + tempX + 'px,' + tempY + 'px)'; Blockly.utils.dom.setCssTransform(this.outerSvg_, transform); }; /** * Recalculate the scrollbar's location and its length. - * @param {Object=} opt_metrics A data structure of from the describing all the - * required dimensions. If not provided, it will be fetched from the host - * object. + * @param {Blockly.utils.Metrics=} opt_metrics A data structure of from the + * describing all the required dimensions. If not provided, it will be + * fetched from the host object. */ Blockly.Scrollbar.prototype.resize = function(opt_metrics) { // Determine the location, height and width of the host element. @@ -450,8 +457,8 @@ Blockly.Scrollbar.prototype.resize = function(opt_metrics) { /** * Recalculate a horizontal scrollbar's location and length. - * @param {!Object} hostMetrics A data structure describing all the - * required dimensions, possibly fetched from the host object. + * @param {!Blockly.utils.Metrics} hostMetrics A data structure describing all + * the required dimensions, possibly fetched from the host object. * @private */ Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) { @@ -463,8 +470,8 @@ Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) { /** * Recalculate a horizontal scrollbar's location on the screen and path length. * This should be called when the layout or size of the window has changed. - * @param {!Object} hostMetrics A data structure describing all the - * required dimensions, possibly fetched from the host object. + * @param {!Blockly.utils.Metrics} hostMetrics A data structure describing all + * the required dimensions, possibly fetched from the host object. */ Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { var viewSize = hostMetrics.viewWidth - 1; @@ -482,7 +489,7 @@ Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { // Horizontal toolbar should always be just above the bottom of the workspace. var yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight - Blockly.Scrollbar.scrollbarThickness - 0.5; - this.setPosition_(xCoordinate, yCoordinate); + this.setPosition(xCoordinate, yCoordinate); // If the view has been resized, a content resize will also be necessary. The // reverse is not true. @@ -492,8 +499,8 @@ Blockly.Scrollbar.prototype.resizeViewHorizontal = function(hostMetrics) { /** * Recalculate a horizontal scrollbar's location within its path and length. * This should be called when the contents of the workspace have changed. - * @param {!Object} hostMetrics A data structure describing all the - * required dimensions, possibly fetched from the host object. + * @param {!Blockly.utils.Metrics} hostMetrics A data structure describing all + * the required dimensions, possibly fetched from the host object. */ Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) { if (!this.pair_) { @@ -503,24 +510,24 @@ Blockly.Scrollbar.prototype.resizeContentHorizontal = function(hostMetrics) { this.setVisible(this.scrollViewSize_ < hostMetrics.contentWidth); } - this.ratio_ = this.scrollViewSize_ / hostMetrics.contentWidth; - if (this.ratio_ == -Infinity || this.ratio_ == Infinity || - isNaN(this.ratio_)) { - this.ratio_ = 0; + this.ratio = this.scrollViewSize_ / hostMetrics.contentWidth; + if (this.ratio == -Infinity || this.ratio == Infinity || + isNaN(this.ratio)) { + this.ratio = 0; } - var handleLength = hostMetrics.viewWidth * this.ratio_; + var handleLength = hostMetrics.viewWidth * this.ratio; this.setHandleLength_(Math.max(0, handleLength)); var handlePosition = (hostMetrics.viewLeft - hostMetrics.contentLeft) * - this.ratio_; + this.ratio; this.setHandlePosition(this.constrainHandle_(handlePosition)); }; /** * Recalculate a vertical scrollbar's location and length. - * @param {!Object} hostMetrics A data structure describing all the - * required dimensions, possibly fetched from the host object. + * @param {!Blockly.utils.Metrics} hostMetrics A data structure describing all + * the required dimensions, possibly fetched from the host object. * @private */ Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) { @@ -532,8 +539,8 @@ Blockly.Scrollbar.prototype.resizeVertical_ = function(hostMetrics) { /** * Recalculate a vertical scrollbar's location on the screen and path length. * This should be called when the layout or size of the window has changed. - * @param {!Object} hostMetrics A data structure describing all the - * required dimensions, possibly fetched from the host object. + * @param {!Blockly.utils.Metrics} hostMetrics A data structure describing all + * the required dimensions, possibly fetched from the host object. */ Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { var viewSize = hostMetrics.viewHeight - 1; @@ -549,7 +556,7 @@ Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { Blockly.Scrollbar.scrollbarThickness - 1; } var yCoordinate = hostMetrics.absoluteTop + 0.5; - this.setPosition_(xCoordinate, yCoordinate); + this.setPosition(xCoordinate, yCoordinate); // If the view has been resized, a content resize will also be necessary. The // reverse is not true. @@ -559,8 +566,8 @@ Blockly.Scrollbar.prototype.resizeViewVertical = function(hostMetrics) { /** * Recalculate a vertical scrollbar's location within its path and length. * This should be called when the contents of the workspace have changed. - * @param {!Object} hostMetrics A data structure describing all the - * required dimensions, possibly fetched from the host object. + * @param {!Blockly.utils.Metrics} hostMetrics A data structure describing all + * the required dimensions, possibly fetched from the host object. */ Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) { if (!this.pair_) { @@ -568,17 +575,17 @@ Blockly.Scrollbar.prototype.resizeContentVertical = function(hostMetrics) { this.setVisible(this.scrollViewSize_ < hostMetrics.contentHeight); } - this.ratio_ = this.scrollViewSize_ / hostMetrics.contentHeight; - if (this.ratio_ == -Infinity || this.ratio_ == Infinity || - isNaN(this.ratio_)) { - this.ratio_ = 0; + this.ratio = this.scrollViewSize_ / hostMetrics.contentHeight; + if (this.ratio == -Infinity || this.ratio == Infinity || + isNaN(this.ratio)) { + this.ratio = 0; } - var handleLength = hostMetrics.viewHeight * this.ratio_; + var handleLength = hostMetrics.viewHeight * this.ratio; this.setHandleLength_(Math.max(0, handleLength)); var handlePosition = (hostMetrics.viewTop - hostMetrics.contentTop) * - this.ratio_; + this.ratio; this.setHandlePosition(this.constrainHandle_(handlePosition)); }; @@ -844,7 +851,7 @@ Blockly.Scrollbar.prototype.onScroll_ = function() { * scrollbar handle. */ Blockly.Scrollbar.prototype.set = function(value) { - this.setHandlePosition(this.constrainHandle_(value * this.ratio_)); + this.setHandlePosition(this.constrainHandle_(value * this.ratio)); this.onScroll_(); }; diff --git a/core/theme.js b/core/theme.js index 311782f55..f66021f4d 100644 --- a/core/theme.js +++ b/core/theme.js @@ -11,6 +11,7 @@ goog.provide('Blockly.Theme'); +goog.require('Blockly.registry'); goog.require('Blockly.utils'); goog.require('Blockly.utils.colour'); goog.require('Blockly.utils.object'); @@ -73,6 +74,9 @@ Blockly.Theme = function(name, opt_blockStyles, opt_categoryStyles, * @package */ this.startHats = null; + + // Register the theme by name. + Blockly.registry.register(Blockly.registry.Type.THEME, name, this); }; /** @@ -97,22 +101,22 @@ Blockly.Theme.CategoryStyle; /** * A component style. * @typedef {{ - * workspaceBackgroundColour:string?, - * toolboxBackgroundColour:string?, - * toolboxForegroundColour:string?, - * flyoutBackgroundColour:string?, - * flyoutForegroundColour:string?, - * flyoutOpacity:number?, - * scrollbarColour:string?, - * scrollbarOpacity:number?, - * insertionMarkerColour:string?, - * insertionMarkerOpacity:number?, - * markerColour:string?, - * cursorColour:string?, - * selectedGlowColour:string?, - * selectedGlowOpacity:number?, - * replacementGlowColour:string?, - * replacementGlowOpacity:number? + * workspaceBackgroundColour:?string, + * toolboxBackgroundColour:?string, + * toolboxForegroundColour:?string, + * flyoutBackgroundColour:?string, + * flyoutForegroundColour:?string, + * flyoutOpacity:?number, + * scrollbarColour:?string, + * scrollbarOpacity:?number, + * insertionMarkerColour:?string, + * insertionMarkerOpacity:?number, + * markerColour:?string, + * cursorColour:?string, + * selectedGlowColour:?string, + * selectedGlowOpacity:?number, + * replacementGlowColour:?string, + * replacementGlowOpacity:?number * }} */ Blockly.Theme.ComponentStyle; @@ -120,9 +124,9 @@ Blockly.Theme.ComponentStyle; /** * A font style. * @typedef {{ - * family:string?, - * weight:string?, - * size:number? + * family:?string, + * weight:?string, + * size:?number * }} */ Blockly.Theme.FontStyle; @@ -206,11 +210,16 @@ Blockly.Theme.prototype.setStartHats = function(startHats) { Blockly.Theme.defineTheme = function(name, themeObj) { var theme = new Blockly.Theme(name); var base = themeObj['base']; - if (base && base instanceof Blockly.Theme) { - Blockly.utils.object.deepMerge(theme, base); - theme.name = name; + if (base) { + if (typeof base == "string") { + base = Blockly.registry.getObject(Blockly.registry.Type.THEME, base); + } + if (base instanceof Blockly.Theme) { + Blockly.utils.object.deepMerge(theme, base); + theme.name = name; + } } - + Blockly.utils.object.deepMerge(theme.blockStyles, themeObj['blockStyles']); Blockly.utils.object.deepMerge(theme.categoryStyles, @@ -222,5 +231,6 @@ Blockly.Theme.defineTheme = function(name, themeObj) { if (themeObj['startHats'] != null) { theme.startHats = themeObj['startHats']; } + return theme; }; diff --git a/core/toolbox.js b/core/toolbox.js index c79e5f585..2499fa526 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -16,6 +16,7 @@ goog.require('Blockly.Css'); goog.require('Blockly.Events'); goog.require('Blockly.Events.Ui'); goog.require('Blockly.navigation'); +goog.require('Blockly.registry'); goog.require('Blockly.Touch'); goog.require('Blockly.tree.TreeControl'); goog.require('Blockly.tree.TreeNode'); @@ -25,6 +26,12 @@ goog.require('Blockly.utils.colour'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Rect'); +goog.require('Blockly.utils.toolbox'); + +goog.requireType('Blockly.IBlocklyActionable'); +goog.requireType('Blockly.IDeleteArea'); +goog.requireType('Blockly.IStyleable'); +goog.requireType('Blockly.IToolbox'); /** @@ -33,6 +40,10 @@ goog.require('Blockly.utils.Rect'); * @param {!Blockly.WorkspaceSvg} workspace The workspace in which to create new * blocks. * @constructor + * @implements {Blockly.IBlocklyActionable} + * @implements {Blockly.IDeleteArea} + * @implements {Blockly.IStyleable} + * @implements {Blockly.IToolbox} */ Blockly.Toolbox = function(workspace) { /** @@ -106,34 +117,27 @@ Blockly.Toolbox = function(workspace) { * @private */ this.flyout_ = null; + + /** + * Width of the toolbox, which changes only in vertical layout. + * @type {number} + */ + this.width = 0; + + /** + * Height of the toolbox, which changes only in horizontal layout. + * @type {number} + */ + this.height = 0; + + /** + * The TreeNode most recently selected. + * @type {Blockly.tree.BaseNode} + * @private + */ + this.lastCategory_ = null; }; -/** - * Width of the toolbox, which changes only in vertical layout. - * @type {number} - */ -Blockly.Toolbox.prototype.width = 0; - -/** - * Height of the toolbox, which changes only in horizontal layout. - * @type {number} - */ -Blockly.Toolbox.prototype.height = 0; - -/** - * The SVG group currently selected. - * @type {SVGGElement} - * @private - */ -Blockly.Toolbox.prototype.selectedOption_ = null; - -/** - * The tree node most recently selected. - * @type {Blockly.tree.BaseNode} - * @private - */ -Blockly.Toolbox.prototype.lastCategory_ = null; - /** * Initializes the toolbox. * @throws {Error} If missing a require for both `Blockly.HorizontalFlyout` and @@ -179,7 +183,7 @@ Blockly.Toolbox.prototype.init = function() { 'rendererOverrides': workspace.options.rendererOverrides })); workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; - + if (workspace.horizontalLayout) { if (!Blockly.HorizontalFlyout) { throw Error('Missing require for Blockly.HorizontalFlyout'); @@ -195,23 +199,23 @@ Blockly.Toolbox.prototype.init = function() { throw Error('One of Blockly.VerticalFlyout or Blockly.Horizontal must be' + 'required.'); } - + // Insert the flyout after the workspace. Blockly.utils.dom.insertAfter(this.flyout_.createDom('svg'), svg); this.flyout_.init(workspace); - this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif'; this.config_['cssCollapsedFolderIcon'] = 'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr'); - this.renderTree(workspace.options.languageTree); + this.render(workspace.options.languageTree); }; /** * Fill the toolbox with categories and blocks. - * @param {Node} languageTree DOM tree of blocks. + * @param {Array.} toolboxDef Array holding objects + * containing information on the contents of the toolbox. * @package */ -Blockly.Toolbox.prototype.renderTree = function(languageTree) { +Blockly.Toolbox.prototype.render = function(toolboxDef) { if (this.tree_) { this.tree_.dispose(); // Delete any existing content. this.lastCategory_ = null; @@ -223,13 +227,12 @@ Blockly.Toolbox.prototype.renderTree = function(languageTree) { tree.onBeforeSelected(this.handleBeforeTreeSelected_); tree.onAfterSelected(this.handleAfterTreeSelected_); var openNode = null; - if (languageTree) { - this.tree_.blocks = []; + if (toolboxDef) { + this.tree_.contents = []; this.hasColours_ = false; - openNode = this.syncTrees_( - languageTree, this.tree_, this.workspace_.options.pathToMedia); + openNode = this.createTree_(toolboxDef, this.tree_); - if (this.tree_.blocks.length) { + if (this.tree_.contents.length) { throw Error('Toolbox cannot have both blocks and categories ' + 'in the root level.'); } @@ -252,6 +255,156 @@ Blockly.Toolbox.prototype.renderTree = function(languageTree) { } }; +/** + * Create the toolbox tree. + * @param {Array.} toolboxDef List of objects + * holding information on toolbox contents. + * @param {!Blockly.tree.BaseNode} treeOut The output tree for the toolbox. Due + * to the recursive nature of this function, treeOut can be either the root of + * the tree (Blockly.tree.TreeControl) or a child node of the tree + * (Blockly.tree.TreeNode). These nodes are built from the toolboxDef. + * @return {Blockly.tree.BaseNode} The TreeNode to expand when the toolbox is + * first loaded (or null). + * @private + */ +Blockly.Toolbox.prototype.createTree_ = function(toolboxDef, treeOut) { + var openNode = null; + var lastElement = null; + if (!toolboxDef) { + return null; + } + + for (var i = 0, childIn; (childIn = toolboxDef[i]); i++) { + switch (childIn['kind'].toUpperCase()) { + case 'CATEGORY': + var categoryInfo = /** @type {Blockly.utils.toolbox.Category} */ (childIn); + openNode = this.addCategory_(categoryInfo, treeOut) || openNode; + lastElement = childIn; + break; + case 'SEP': + var separatorInfo = /** @type {Blockly.utils.toolbox.Separator} */ (childIn); + lastElement = this.addSeparator_(separatorInfo, treeOut, lastElement) || lastElement; + break; + case 'BLOCK': + case 'SHADOW': + case 'LABEL': + case 'BUTTON': + treeOut.contents.push(childIn); + lastElement = childIn; + break; + } + } + return openNode; +}; + +/** + * Add a category to the toolbox tree. + * @param {!Blockly.utils.toolbox.Category} categoryInfo The object holding + * information on the category. + * @param {!Blockly.tree.BaseNode} treeOut The TreeControl or TreeNode + * object built from the childNodes. + * @return {Blockly.tree.BaseNode} TreeNode to open at startup (or null). + * @private + */ +Blockly.Toolbox.prototype.addCategory_ = function(categoryInfo, treeOut) { + var openNode = null; + // Decode the category name for any potential message references + // (eg. `%{BKY_CATEGORY_NAME_LOGIC}`). + var categoryName = Blockly.utils.replaceMessageReferences(categoryInfo['name']); + + // Create and add the tree node for the category. + var childOut = this.tree_.createNode(categoryName); + childOut.onSizeChanged(this.handleNodeSizeChanged_); + childOut.contents = []; + treeOut.add(childOut); + + var custom = categoryInfo['custom']; + + if (custom) { + // Variables and procedures are special dynamic categories. + childOut.contents = custom; + } else { + openNode = this.createTree_(categoryInfo['contents'], childOut) || openNode; + } + this.setColourOrStyle_(categoryInfo, childOut, categoryName); + openNode = this.setExpanded_(categoryInfo, childOut) || openNode; + return openNode; +}; + +/** + * Add either the colour or the style for a category. + * @param {!Blockly.utils.toolbox.Category} categoryInfo The object holding + * information on the category. + * @param {!Blockly.tree.TreeNode} childOut The TreeNode for a category. + * @param {string} categoryName The name of the category. + * @private + */ +Blockly.Toolbox.prototype.setColourOrStyle_ = function( + categoryInfo, childOut, categoryName) { + var styleName = categoryInfo['categorystyle']; + var colour = categoryInfo['colour']; + + if (colour && styleName) { + childOut.hexColour = ''; + console.warn('Toolbox category "' + categoryName + + '" must not have both a style and a colour'); + } else if (styleName) { + this.setColourFromStyle_(styleName, childOut, categoryName); + } else { + this.setColour_(colour, childOut, categoryName); + } +}; + +/** + * Add a separator to the toolbox tree if it is between categories. Otherwise, + * add the separator to the list of contents. + * @param {!Blockly.utils.toolbox.Separator} separatorInfo The object holding + * information on the separator. + * @param {!Blockly.tree.BaseNode} treeOut The TreeControl or TreeNode + * object built from the childNodes. + * @param {Object} lastElement The last element to be added to the tree. + * @return {Object} The last element to be added to the tree, or + * null. + * @private + */ +Blockly.Toolbox.prototype.addSeparator_ = function( + separatorInfo, treeOut, lastElement) { + if (lastElement && lastElement['kind'].toUpperCase() == 'CATEGORY') { + // Separator between two categories. + // + treeOut.add(new Blockly.Toolbox.TreeSeparator( + /** @type {!Blockly.tree.BaseNode.Config} */ + (this.treeSeparatorConfig_))); + } else { + // Otherwise add to contents array. + treeOut.contents.push(separatorInfo); + return separatorInfo; + } + return null; +}; + +/** + * Checks whether a node should be expanded, and expands if necessary. + * @param {!Blockly.utils.toolbox.Category} categoryInfo The child to expand. + * @param {!Blockly.tree.TreeNode} childOut The TreeNode created from childIn. + * @return {Blockly.tree.BaseNode} TreeNode to open at startup (or null). + * @private + */ +Blockly.Toolbox.prototype.setExpanded_ = function(categoryInfo, childOut) { + var openNode = null; + if (categoryInfo['expanded'] == 'true') { + if (childOut.contents.length) { + // This is a category that directly contains blocks. + // After the tree is rendered, open this category and show flyout. + openNode = childOut; + } + childOut.setExpanded(true); + } else { + childOut.setExpanded(false); + } + return openNode; +}; + /** * Handle the before tree item selected action. * @param {Blockly.tree.BaseNode} node The newly selected node. @@ -283,8 +436,8 @@ Blockly.Toolbox.prototype.handleBeforeTreeSelected_ = function(node) { */ Blockly.Toolbox.prototype.handleAfterTreeSelected_ = function( oldNode, newNode) { - if (newNode && newNode.blocks && newNode.blocks.length) { - this.flyout_.show(newNode.blocks); + if (newNode && newNode.contents && newNode.contents.length) { + this.flyout_.show(newNode.contents); // Scroll the flyout to the top if the category has changed. if (this.lastCategory_ != newNode) { this.flyout_.scrollToStart(); @@ -358,6 +511,14 @@ Blockly.Toolbox.prototype.dispose = function() { this.lastCategory_ = null; }; +/** + * Toggles the visibility of the toolbox. + * @param {boolean} isVisible True if toolbox should be visible. + */ +Blockly.Toolbox.prototype.setVisible = function(isVisible) { + this.HtmlDiv.style.display = isVisible ? 'block' : 'none'; +}; + /** * Get the width of the toolbox. * @return {number} The width of the toolbox. @@ -414,91 +575,6 @@ Blockly.Toolbox.prototype.position = function() { this.flyout_.position(); }; -/** - * Sync trees of the toolbox. - * @param {!Node} treeIn DOM tree of blocks. - * @param {!Blockly.tree.BaseNode} treeOut The TreeControl or TreeNode - * object built from treeIn. - * @param {string} pathToMedia The path to the Blockly media directory. - * @return {Blockly.tree.BaseNode} Tree node to open at startup (or null). - * @private - */ -Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { - var openNode = null; - var lastElement = null; - for (var i = 0, childIn; (childIn = treeIn.childNodes[i]); i++) { - if (!childIn.tagName) { - // Skip over text. - continue; - } - switch (childIn.tagName.toUpperCase()) { - case 'CATEGORY': - // Decode the category name for any potential message references - // (eg. `%{BKY_CATEGORY_NAME_LOGIC}`). - var categoryName = Blockly.utils.replaceMessageReferences( - childIn.getAttribute('name')); - var childOut = this.tree_.createNode(categoryName); - childOut.onSizeChanged(this.handleNodeSizeChanged_); - childOut.blocks = []; - treeOut.add(childOut); - var custom = childIn.getAttribute('custom'); - if (custom) { - // Variables and procedures are special dynamic categories. - childOut.blocks = custom; - } else { - var newOpenNode = this.syncTrees_(childIn, childOut, pathToMedia); - if (newOpenNode) { - openNode = newOpenNode; - } - } - - var styleName = childIn.getAttribute('categorystyle'); - var colour = childIn.getAttribute('colour'); - - if (colour && styleName) { - childOut.hexColour = ''; - console.warn('Toolbox category "' + categoryName + - '" can not have both a style and a colour'); - } else if (styleName) { - this.setColourFromStyle_(styleName, childOut, categoryName); - } else { - this.setColour_(colour, childOut, categoryName); - } - - if (childIn.getAttribute('expanded') == 'true') { - if (childOut.blocks.length) { - // This is a category that directly contains blocks. - // After the tree is rendered, open this category and show flyout. - openNode = childOut; - } - childOut.setExpanded(true); - } else { - childOut.setExpanded(false); - } - lastElement = childIn; - break; - case 'SEP': - if (lastElement && lastElement.tagName.toUpperCase() == 'CATEGORY') { - // Separator between two categories. - // - treeOut.add(new Blockly.Toolbox.TreeSeparator( - /** @type {!Blockly.tree.BaseNode.Config} */ - (this.treeSeparatorConfig_))); - break; - } - // Otherwise falls through. - case 'BLOCK': - case 'SHADOW': - case 'LABEL': - case 'BUTTON': - treeOut.blocks.push(childIn); - lastElement = childIn; - break; - } - } - return openNode; -}; - /** * Sets the colour on the category. * @param {number|string} colourValue HSV hue value (0 to 360), #RRGGBB string, @@ -581,7 +657,7 @@ Blockly.Toolbox.prototype.updateColourFromTheme_ = function(opt_tree) { * Updates the category colours and background colour of selected categories. * @package */ -Blockly.Toolbox.prototype.updateColourFromTheme = function() { +Blockly.Toolbox.prototype.refreshTheme = function() { var tree = this.tree_; if (tree) { this.updateColourFromTheme_(tree); @@ -697,8 +773,8 @@ Blockly.Toolbox.prototype.getClientRect = function() { */ Blockly.Toolbox.prototype.refreshSelection = function() { var selectedItem = this.tree_.getSelectedItem(); - if (selectedItem && selectedItem.blocks) { - this.flyout_.show(selectedItem.blocks); + if (selectedItem && selectedItem.contents) { + this.flyout_.show(selectedItem.contents); } }; @@ -835,8 +911,7 @@ Blockly.Css.register([ '.blocklyTreeLabel {', 'cursor: default;', - 'font-family: sans-serif;', - 'font-size: 16px;', + 'font: 16px sans-serif;', 'padding: 0 3px;', 'vertical-align: middle;', '}', @@ -850,3 +925,6 @@ Blockly.Css.register([ '}' /* eslint-enable indent */ ]); + +Blockly.registry.register(Blockly.registry.Type.TOOLBOX, + Blockly.registry.DEFAULT, Blockly.Toolbox); diff --git a/core/trashcan.js b/core/trashcan.js index 5c45cf82b..107b9d602 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -17,11 +17,14 @@ goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.Rect'); goog.require('Blockly.Xml'); +goog.requireType('Blockly.IDeleteArea'); + /** * Class for a trash can. * @param {!Blockly.WorkspaceSvg} workspace The workspace to sit in. * @constructor + * @implements {Blockly.IDeleteArea} */ Blockly.Trashcan = function(workspace) { /** diff --git a/core/ui_menu_utils.js b/core/ui_menu_utils.js deleted file mode 100644 index 321501e23..000000000 --- a/core/ui_menu_utils.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Utility methods for working with the Closure menu - * (goog.ui.menu). - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -/** - * @name Blockly.utils.uiMenu - * @namespace - */ -goog.provide('Blockly.utils.uiMenu'); - -goog.require('Blockly.utils.style'); - - -/** - * Get the size of a rendered goog.ui.Menu. - * @param {!Blockly.Menu} menu The menu to measure. - * @return {!Blockly.utils.Size} Object with width and height properties. - * @package - */ -Blockly.utils.uiMenu.getSize = function(menu) { - var menuDom = menu.getElement(); - var menuSize = Blockly.utils.style.getSize(/** @type {!Element} */ (menuDom)); - // Recalculate height for the total content, not only box height. - menuSize.height = menuDom.scrollHeight; - return menuSize; -}; - -/** - * Adjust the bounding boxes used to position the widget div to deal with RTL - * goog.ui.Menu positioning. In RTL mode the menu renders down and to the left - * of its start point, instead of down and to the right. Adjusting all of the - * bounding boxes accordingly allows us to use the same code for all widgets. - * This function in-place modifies the provided bounding boxes. - * @param {!Object} viewportBBox The bounding rectangle of the current viewport, - * in window coordinates. - * @param {!Object} anchorBBox The bounding rectangle of the anchor, in window - * coordinates. - * @param {!Blockly.utils.Size} menuSize The size of the menu that is inside the - * widget div, in window coordinates. - * @package - */ -Blockly.utils.uiMenu.adjustBBoxesForRTL = function(viewportBBox, anchorBBox, - menuSize) { - anchorBBox.left += menuSize.width; - anchorBBox.right += menuSize.width; - viewportBBox.left += menuSize.width; - viewportBBox.right += menuSize.width; -}; diff --git a/core/utils.js b/core/utils.js index cfd047ca1..4bbffdca5 100644 --- a/core/utils.js +++ b/core/utils.js @@ -23,6 +23,7 @@ goog.require('Blockly.constants'); goog.require('Blockly.utils.colour'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.global'); +goog.require('Blockly.utils.Rect'); goog.require('Blockly.utils.string'); goog.require('Blockly.utils.style'); goog.require('Blockly.utils.userAgent'); @@ -496,19 +497,19 @@ Blockly.utils.runAfterPageLoad = function(fn) { /** * Get the position of the current viewport in window coordinates. This takes * scroll into account. - * @return {!Object} An object containing window width, height, and scroll - * position in window coordinates. + * @return {!Blockly.utils.Rect} An object containing window width, height, and + * scroll position in window coordinates. * @package */ Blockly.utils.getViewportBBox = function() { // Pixels, in window coordinates. var scrollOffset = Blockly.utils.style.getViewportPageOffset(); - return { - right: document.documentElement.clientWidth + scrollOffset.x, - bottom: document.documentElement.clientHeight + scrollOffset.y, - top: scrollOffset.y, - left: scrollOffset.x - }; + return new Blockly.utils.Rect( + scrollOffset.y, + document.documentElement.clientHeight + scrollOffset.y, + scrollOffset.x, + document.documentElement.clientWidth + scrollOffset.x + ); }; /** diff --git a/core/utils/aria.js b/core/utils/aria.js index eab295f12..f455b9928 100644 --- a/core/utils/aria.js +++ b/core/utils/aria.js @@ -86,6 +86,9 @@ Blockly.utils.aria.State = { // Value: integer. COLCOUNT: 'colcount', + // ARIA state for a disabled item. Value: one of {true, false}. + DISABLED: 'disabled', + // ARIA state for setting whether the element like a tree node is expanded. // Value: one of {true, false, undefined}. EXPANDED: 'expanded', diff --git a/core/utils/dom.js b/core/utils/dom.js index c68f98778..202889c05 100644 --- a/core/utils/dom.js +++ b/core/utils/dom.js @@ -44,7 +44,7 @@ Blockly.utils.dom.XLINK_NS = 'http://www.w3.org/1999/xlink'; * https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType * @enum {number} */ -Blockly.utils.dom.Node = { +Blockly.utils.dom.NodeType = { ELEMENT_NODE: 1, TEXT_NODE: 3, COMMENT_NODE: 8, @@ -76,10 +76,10 @@ Blockly.utils.dom.canvasContext_ = null; * Helper method for creating SVG elements. * @param {string} name Element's tag name. * @param {!Object} attrs Dictionary of attribute names and values. - * @param {Element} parent Optional parent on which to append the element. + * @param {Element=} opt_parent Optional parent on which to append the element. * @return {!SVGElement} Newly created SVG element. */ -Blockly.utils.dom.createSvgElement = function(name, attrs, parent) { +Blockly.utils.dom.createSvgElement = function(name, attrs, opt_parent) { var e = /** @type {!SVGElement} */ (document.createElementNS(Blockly.utils.dom.SVG_NS, name)); for (var key in attrs) { @@ -91,8 +91,8 @@ Blockly.utils.dom.createSvgElement = function(name, attrs, parent) { if (document.body.runtimeStyle) { // Indicates presence of IE-only attr. e.runtimeStyle = e.currentStyle = e.style; } - if (parent) { - parent.appendChild(e); + if (opt_parent) { + opt_parent.appendChild(e); } return e; }; @@ -192,7 +192,7 @@ Blockly.utils.dom.insertAfter = function(newNode, refNode) { */ Blockly.utils.dom.containsNode = function(parent, descendant) { return !!(parent.compareDocumentPosition(descendant) & - Blockly.utils.dom.Node.DOCUMENT_POSITION_CONTAINED_BY); + Blockly.utils.dom.NodeType.DOCUMENT_POSITION_CONTAINED_BY); }; /** diff --git a/core/utils/idgenerator.js b/core/utils/idgenerator.js index 81bd405a6..e17c51f31 100644 --- a/core/utils/idgenerator.js +++ b/core/utils/idgenerator.js @@ -29,5 +29,5 @@ Blockly.utils.IdGenerator.nextId_ = 0; * @return {string} The next unique identifier. */ Blockly.utils.IdGenerator.getNextUniqueId = function() { - return 'blockly:' + (Blockly.utils.IdGenerator.nextId_++).toString(36); + return 'blockly-' + (Blockly.utils.IdGenerator.nextId_++).toString(36); }; diff --git a/core/utils/metrics.js b/core/utils/metrics.js new file mode 100644 index 000000000..274c382c6 --- /dev/null +++ b/core/utils/metrics.js @@ -0,0 +1,124 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Workspace metrics definitions. + * @author samelh@google.com (Sam El-Husseini) + */ +'use strict'; + +goog.provide('Blockly.utils.Metrics'); + + +/** + * @record + */ +Blockly.utils.Metrics = function() {}; + +/** + * Height of the visible portion of the workspace. + * @type {number} + */ +Blockly.utils.Metrics.prototype.viewHeight; + +/** + * Width of the visible portion of the workspace. + * @type {number} + */ +Blockly.utils.Metrics.prototype.viewWidth; + +/** + * Height of the content. + * @type {number} + */ +Blockly.utils.Metrics.prototype.contentHeight; + +/** + * Width of the content. + * @type {number} + */ +Blockly.utils.Metrics.prototype.contentWidth; + +/** + * Top-edge of the visible portion of the workspace, relative to the workspace + * origin. + * @type {number} + */ +Blockly.utils.Metrics.prototype.viewTop; + +/** + * Left-edge of the visible portion of the workspace, relative to the workspace + * origin. + * @type {number} + */ +Blockly.utils.Metrics.prototype.viewLeft; + +/** + * Top-edge of the content, relative to the workspace origin. + * @type {number} + */ +Blockly.utils.Metrics.prototype.contentTop; + +/** + * Left-edge of the content relative to the workspace origin. + * @type {number} + */ +Blockly.utils.Metrics.prototype.contentLeft; + +/** + * Top-edge of the visible portion of the workspace, relative to the blocklyDiv. + * @type {number} + */ +Blockly.utils.Metrics.prototype.absoluteTop; + +/** + * Left-edge of the visible portion of the workspace, relative to the + * blocklyDiv. + * @type {number} + */ +Blockly.utils.Metrics.prototype.absoluteLeft; + +/** + * Height of the Blockly div (the view + the toolbox, simple of otherwise). + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.svgHeight; + +/** + * Width of the Blockly div (the view + the toolbox, simple or otherwise). + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.svgWidth; + +/** + * Width of the toolbox, if it exists. Otherwise zero. + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.toolboxWidth; + +/** + * Height of the toolbox, if it exists. Otherwise zero. + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.toolboxHeight; + +/** + * Top, bottom, left or right. Use TOOLBOX_AT constants to compare. + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.toolboxPosition; + +/** + * Width of the flyout if it is always open. Otherwise zero. + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.flyoutWidth; + +/** + * Height of the flyout if it is always open. Otherwise zero. + * @type {number|undefined} + */ +Blockly.utils.Metrics.prototype.flyoutHeight; diff --git a/core/utils/object.js b/core/utils/object.js index 0212f3fdc..e382b9ee5 100644 --- a/core/utils/object.js +++ b/core/utils/object.js @@ -45,7 +45,7 @@ Blockly.utils.object.mixin = function(target, source) { */ Blockly.utils.object.deepMerge = function(target, source) { for (var x in source) { - if (typeof source[x] === 'object') { + if (source[x] != null && typeof source[x] === 'object') { target[x] = Blockly.utils.object.deepMerge( target[x] || Object.create(null), source[x]); } else { diff --git a/core/utils/toolbox.js b/core/utils/toolbox.js new file mode 100644 index 000000000..a7b6a62b8 --- /dev/null +++ b/core/utils/toolbox.js @@ -0,0 +1,169 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview Utility functions for the toolbox and flyout. + * @author aschmiedt@google.com (Abby Schmiedt) + */ +'use strict'; + +goog.provide('Blockly.utils.toolbox'); + + +/** + * The information needed to create a block in the toolbox. + * @typedef {{ + * kind:string, + * blockxml:(?string|Node), + * type: ?string, + * gap: (?string|?number), + * disabled: (?string|?boolean) + * }} + */ +Blockly.utils.toolbox.Block; + +/** + * The information needed to create a separator in the toolbox. + * @typedef {{ + * kind:string, + * gap:?number + * }} + */ +Blockly.utils.toolbox.Separator; + +/** + * The information needed to create a button in the toolbox. + * @typedef {{ + * kind:string, + * text:string, + * callbackkey:string + * }} + */ +Blockly.utils.toolbox.Button; + +/** + * The information needed to create a label in the toolbox. + * @typedef {{ + * kind:string, + * text:string + * }} + */ +Blockly.utils.toolbox.Label; + +/** + * The information needed to create a category in the toolbox. + * @typedef {{ + * kind:string, + * name:string, + * categorystyle:?string, + * colour:?string, + * contents:Array. + * }} + */ +Blockly.utils.toolbox.Category; + +/** + * Any information that can be used to create an item in the toolbox. + * @typedef {Blockly.utils.toolbox.Block| + * Blockly.utils.toolbox.Separator| + * Blockly.utils.toolbox.Button| + * Blockly.utils.toolbox.Label| + * Blockly.utils.toolbox.Category} + */ +Blockly.utils.toolbox.Toolbox; + +/** + * All of the different types that can create a toolbox. + * @typedef {Node| + * NodeList| + * Array.| + * Array.} + */ +Blockly.utils.toolbox.ToolboxDefinition; + + +/** + * Parse the provided toolbox definition into a consistent format. + * @param {Blockly.utils.toolbox.ToolboxDefinition} toolboxDef The definition of the + * toolbox in one of its many forms. + * @return {Array.} Array of JSON holding + * information on toolbox contents. + * @package + */ +Blockly.utils.toolbox.convertToolboxToJSON = function(toolboxDef) { + if (!toolboxDef) { + return null; + } + // If it is an array of JSON, then it is already in the correct format. + if (Array.isArray(toolboxDef) && toolboxDef.length && !(toolboxDef[0].nodeType)) { + if (Blockly.utils.toolbox.hasCategories(toolboxDef)) { + // TODO: Remove after #3985 has been looked into. + console.warn('Due to some performance issues, defining a toolbox using' + + 'JSON is not ready yet. Please define your toolbox using xml.'); + } + return /** @type {!Array.} */ (toolboxDef); + } + + return Blockly.utils.toolbox.toolboxXmlToJson_(toolboxDef); +}; + +/** + * Convert the xml for a toolbox to JSON. + * @param {!NodeList|!Node|!Array.} toolboxDef The + * definition of the toolbox in one of its many forms. + * @return {!Array.} A list of objects in the + * toolbox. + * @private + */ +Blockly.utils.toolbox.toolboxXmlToJson_ = function(toolboxDef) { + var arr = []; + // If it is a node it will have children. + var childNodes = toolboxDef.childNodes; + if (!childNodes) { + // Otherwise the toolboxDef is an array or collection. + childNodes = toolboxDef; + } + for (var i = 0, child; (child = childNodes[i]); i++) { + if (!child.tagName) { + continue; + } + var obj = {}; + var tagName = child.tagName.toUpperCase(); + obj['kind'] = tagName; + + // Store the xml for a block + if (tagName == 'BLOCK') { + obj['blockxml'] = child; + } else if (tagName == 'CATEGORY') { + // Get the contents of a category + obj['contents'] = Blockly.utils.toolbox.toolboxXmlToJson_(child); + } + + // Add xml attributes to object + for (var j = 0; j < child.attributes.length; j++) { + var attr = child.attributes[j]; + obj[attr.nodeName] = attr.value; + } + arr.push(obj); + } + return arr; +}; + +/** + * Whether or not the toolbox definition has categories or not. + * @param {Node|Array.} toolboxDef The definition + * of the toolbox. Either in xml or JSON. + * @return {boolean} True if the toolbox has categories. + * @package + */ +Blockly.utils.toolbox.hasCategories = function(toolboxDef) { + if (Array.isArray(toolboxDef)) { + // Search for categories + return !!(toolboxDef.length && toolboxDef[0]['kind'].toUpperCase() == 'CATEGORY'); + } else { + return !!(toolboxDef && toolboxDef.getElementsByTagName('category').length); + } +}; diff --git a/core/utils/useragent.js b/core/utils/useragent.js index 401a49afb..84d036d57 100644 --- a/core/utils/useragent.js +++ b/core/utils/useragent.js @@ -20,6 +20,45 @@ goog.provide('Blockly.utils.userAgent'); goog.require('Blockly.utils.global'); +/** @const {boolean} */ +Blockly.utils.userAgent.IE; + +/** @const {boolean} */ +Blockly.utils.userAgent.EDGE; + +/** @const {boolean} */ +Blockly.utils.userAgent.JAVA_FX; + +/** @const {boolean} */ +Blockly.utils.userAgent.CHROME; + +/** @const {boolean} */ +Blockly.utils.userAgent.WEBKIT; + +/** @const {boolean} */ +Blockly.utils.userAgent.GECKO; + +/** @const {boolean} */ +Blockly.utils.userAgent.ANDROID; + +/** @const {boolean} */ +Blockly.utils.userAgent.IPAD; + +/** @const {boolean} */ +Blockly.utils.userAgent.IPOD; + +/** @const {boolean} */ +Blockly.utils.userAgent.IPHONE; + +/** @const {boolean} */ +Blockly.utils.userAgent.MAC; + +/** @const {boolean} */ +Blockly.utils.userAgent.TABLET; + +/** @const {boolean} */ +Blockly.utils.userAgent.MOBILE; + (function(raw) { Blockly.utils.userAgent.raw = raw; var rawUpper = Blockly.utils.userAgent.raw.toUpperCase(); diff --git a/core/variable_map.js b/core/variable_map.js index 4b6059ead..e2dcafaea 100644 --- a/core/variable_map.js +++ b/core/variable_map.js @@ -378,7 +378,7 @@ Blockly.VariableMap.prototype.getAllVariables = function() { /** * Returns all of the variable names of all types. - * @return {!Array} All of the variable names of all types. + * @return {!Array.} All of the variable names of all types. */ Blockly.VariableMap.prototype.getAllVariableNames = function() { var allNames = []; diff --git a/core/variables.js b/core/variables.js index 271439a60..6aa9374d5 100644 --- a/core/variables.js +++ b/core/variables.js @@ -217,7 +217,7 @@ Blockly.Variables.generateUniqueName = function(workspace) { * will try to generate single letter names in the range a -> z (skip l). It * will start with the character passed to startChar. * @param {string} startChar The character to start the search at. - * @param {!Array} usedNames A list of all of the used names. + * @param {!Array.} usedNames A list of all of the used names. * @return {string} A unique name that is not present in the usedNames array. */ Blockly.Variables.generateUniqueNameFromOptions = function(startChar, usedNames) { @@ -279,7 +279,7 @@ Blockly.Variables.createVariableButtonHandler = function( function(text) { if (text) { var existing = - Blockly.Variables.nameUsedWithAnyType_(text, workspace); + Blockly.Variables.nameUsedWithAnyType(text, workspace); if (existing) { if (existing.type == type) { var msg = Blockly.Msg['VARIABLE_ALREADY_EXISTS'].replace( @@ -327,7 +327,9 @@ Blockly.Variables.createVariable = Blockly.Variables.createVariableButtonHandler; /** - * Rename a variable with the given workspace, variableType, and oldName. + * Opens a prompt that allows the user to enter a new name for a variable. + * Triggers a rename if the new name is valid. Or re-prompts if there is a + * collision. * @param {!Blockly.Workspace} workspace The workspace on which to rename the * variable. * @param {Blockly.VariableModel} variable Variable to rename. @@ -424,9 +426,8 @@ Blockly.Variables.nameUsedWithOtherType_ = function(name, type, workspace) { * variable. * @return {Blockly.VariableModel} The variable with the given name, * or null if none was found. - * @private */ -Blockly.Variables.nameUsedWithAnyType_ = function(name, workspace) { +Blockly.Variables.nameUsedWithAnyType = function(name, workspace) { var allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); diff --git a/core/warning.js b/core/warning.js index 7a5364c5b..915873706 100644 --- a/core/warning.js +++ b/core/warning.js @@ -42,7 +42,7 @@ Blockly.Warning.prototype.collapseHidden = false; /** * Draw the warning icon. * @param {!Element} group The icon group. - * @private + * @protected */ Blockly.Warning.prototype.drawIcon_ = function(group) { // Triangle with rounded corners. @@ -154,16 +154,6 @@ Blockly.Warning.prototype.disposeBubble = function() { this.paragraphElement_ = null; }; -/** - * Bring the warning to the top of the stack when clicked on. - * @param {!Event} _e Mouse up event. - * @private - */ - -Blockly.Warning.prototype.bodyFocus_ = function(_e) { - this.bubble_.promote(); -}; - /** * Set this warning's text. * @param {string} text Warning text (or '' to delete). This supports diff --git a/core/widgetdiv.js b/core/widgetdiv.js index 3529a4400..37631bd2b 100644 --- a/core/widgetdiv.js +++ b/core/widgetdiv.js @@ -20,6 +20,9 @@ goog.provide('Blockly.WidgetDiv'); goog.require('Blockly.utils.style'); +goog.requireType('Blockly.utils.Rect'); +goog.requireType('Blockly.utils.Size'); + /** * The object currently using this container. @@ -80,10 +83,11 @@ Blockly.WidgetDiv.show = function(newOwner, rtl, dispose) { var div = Blockly.WidgetDiv.DIV; div.style.direction = rtl ? 'rtl' : 'ltr'; div.style.display = 'block'; + var mainWorkspace = + /** @type {!Blockly.WorkspaceSvg} */ (Blockly.getMainWorkspace()); Blockly.WidgetDiv.rendererClassName_ = - Blockly.getMainWorkspace().getRenderer().getClassName(); - Blockly.WidgetDiv.themeClassName_ = - Blockly.getMainWorkspace().getTheme().getClassName(); + mainWorkspace.getRenderer().getClassName(); + Blockly.WidgetDiv.themeClassName_ = mainWorkspace.getTheme().getClassName(); Blockly.utils.dom.addClass(div, Blockly.WidgetDiv.rendererClassName_); Blockly.utils.dom.addClass(div, Blockly.WidgetDiv.themeClassName_); }; @@ -113,7 +117,8 @@ Blockly.WidgetDiv.hide = function() { Blockly.utils.dom.removeClass(div, Blockly.WidgetDiv.themeClassName_); Blockly.WidgetDiv.themeClassName_ = ''; } - Blockly.getMainWorkspace().markFocused(); + (/** @type {!Blockly.WorkspaceSvg} */ ( + Blockly.getMainWorkspace())).markFocused(); }; /** @@ -154,12 +159,12 @@ Blockly.WidgetDiv.positionInternal_ = function(x, y, height) { * The widget should be placed adjacent to but not overlapping the anchor * rectangle. The preferred position is directly below and aligned to the left * (LTR) or right (RTL) side of the anchor. - * @param {!Object} viewportBBox The bounding rectangle of the current viewport, + * @param {!Blockly.utils.Rect} viewportBBox The bounding rectangle of the + * current viewport, in window coordinates. + * @param {!Blockly.utils.Rect} anchorBBox The bounding rectangle of the anchor, * in window coordinates. - * @param {!Object} anchorBBox The bounding rectangle of the anchor, in window - * coordinates. - * @param {!Blockly.utils.Size} widgetSize The size of the widget that is inside the - * widget div, in window coordinates. + * @param {!Blockly.utils.Size} widgetSize The size of the widget that is inside + * the widget div, in window coordinates. * @param {boolean} rtl Whether the workspace is in RTL mode. This determines * horizontal alignment. * @package @@ -180,10 +185,10 @@ Blockly.WidgetDiv.positionWithAnchor = function(viewportBBox, anchorBBox, /** * Calculate an x position (in window coordinates) such that the widget will not * be offscreen on the right or left. - * @param {!Object} viewportBBox The bounding rectangle of the current viewport, + * @param {!Blockly.utils.Rect} viewportBBox The bounding rectangle of the + * current viewport, in window coordinates. + * @param {!Blockly.utils.Rect} anchorBBox The bounding rectangle of the anchor, * in window coordinates. - * @param {!Object} anchorBBox The bounding rectangle of the anchor, in window - * coordinates. * @param {Blockly.utils.Size} widgetSize The dimensions of the widget inside the * widget div. * @param {boolean} rtl Whether the Blockly workspace is in RTL mode. @@ -212,10 +217,10 @@ Blockly.WidgetDiv.calculateX_ = function(viewportBBox, anchorBBox, widgetSize, /** * Calculate a y position (in window coordinates) such that the widget will not * be offscreen on the top or bottom. - * @param {!Object} viewportBBox The bounding rectangle of the current viewport, + * @param {!Blockly.utils.Rect} viewportBBox The bounding rectangle of the + * current viewport, in window coordinates. + * @param {!Blockly.utils.Rect} anchorBBox The bounding rectangle of the anchor, * in window coordinates. - * @param {!Object} anchorBBox The bounding rectangle of the anchor, in window - * coordinates. * @param {Blockly.utils.Size} widgetSize The dimensions of the widget inside the * widget div. * @return {number} A valid y-coordinate for the top left corner of the widget diff --git a/core/workspace.js b/core/workspace.js index 28a0ab817..02287e10c 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -18,12 +18,15 @@ goog.require('Blockly.utils'); goog.require('Blockly.utils.math'); goog.require('Blockly.VariableMap'); +goog.requireType('Blockly.IASTNodeLocation'); + /** * Class for a workspace. This is a data structure that contains blocks. * There is no UI, and can be created headlessly. * @param {!Blockly.Options=} opt_options Dictionary of options. * @constructor + * @implements {Blockly.IASTNodeLocation} */ Blockly.Workspace = function(opt_options) { /** @type {string} */ @@ -167,7 +170,7 @@ Blockly.Workspace.prototype.sortObjects_ = function(a, b) { }; /** - * Add a block to the list of top blocks. + * Adds a block to the list of top blocks. * @param {!Blockly.Block} block Block to add. */ Blockly.Workspace.prototype.addTopBlock = function(block) { @@ -175,7 +178,7 @@ Blockly.Workspace.prototype.addTopBlock = function(block) { }; /** - * Remove a block from the list of top blocks. + * Removes a block from the list of top blocks. * @param {!Blockly.Block} block Block to remove. */ Blockly.Workspace.prototype.removeTopBlock = function(block) { @@ -251,7 +254,7 @@ Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { }; /** - * Add a comment to the list of top comments. + * Adds a comment to the list of top comments. * @param {!Blockly.WorkspaceComment} comment comment to add. * @package */ @@ -268,7 +271,7 @@ Blockly.Workspace.prototype.addTopComment = function(comment) { }; /** - * Remove a comment from the list of top comments. + * Removes a comment from the list of top comments. * @param {!Blockly.WorkspaceComment} comment comment to remove. * @package */ @@ -406,17 +409,6 @@ Blockly.Workspace.prototype.deleteVariableById = function(id) { this.variableMap_.deleteVariableById(id); }; -/** - * Deletes a variable and all of its uses from this workspace without asking - * the user for confirmation. - * @param {!Blockly.VariableModel} variable Variable to delete. - * @param {!Array.} uses An array of uses of the variable. - * @private - */ -Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable, uses) { - this.variableMap_.deleteVariableInternal(variable, uses); -}; - /** * Check whether a variable exists with the given name. The check is * case-insensitive. @@ -485,7 +477,7 @@ Blockly.Workspace.prototype.getAllVariables = function() { /** * Returns all variable names of all types. - * @return {!Array} List of all variable names of all types. + * @return {!Array.} List of all variable names of all types. */ Blockly.Workspace.prototype.getAllVariableNames = function() { return this.variableMap_.getAllVariableNames(); @@ -538,8 +530,11 @@ Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { if (!this.options.maxInstances) { return Infinity; } - return (this.options.maxInstances[type] || Infinity) - - this.getBlocksByType(type, false).length; + + var maxInstanceOfType = (this.options.maxInstances[type] !== undefined) ? + this.options.maxInstances[type] : Infinity; + + return maxInstanceOfType - this.getBlocksByType(type, false).length; }; /** diff --git a/core/workspace_audio.js b/core/workspace_audio.js index 9d2b358ff..3de7eb1f4 100644 --- a/core/workspace_audio.js +++ b/core/workspace_audio.js @@ -110,7 +110,7 @@ Blockly.WorkspaceAudio.prototype.preload = function() { } else { sound.pause(); } - + // iOS can only process one sound at a time. Trying to load more than one // corrupts the earlier ones. Just load one and leave the others uncached. if (Blockly.utils.userAgent.IPAD || Blockly.utils.userAgent.IPHONE) { diff --git a/core/workspace_comment_svg.js b/core/workspace_comment_svg.js index f4b97a136..4190074ba 100644 --- a/core/workspace_comment_svg.js +++ b/core/workspace_comment_svg.js @@ -25,6 +25,8 @@ goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Rect'); goog.require('Blockly.WorkspaceComment'); +goog.requireType('Blockly.IBoundedElement'); +goog.requireType('Blockly.ICopyable'); /** * Class for a workspace comment's SVG representation. @@ -35,6 +37,8 @@ goog.require('Blockly.WorkspaceComment'); * @param {string=} opt_id Optional ID. Use this ID if provided, otherwise * create a new ID. * @extends {Blockly.WorkspaceComment} + * @implements {Blockly.IBoundedElement} + * @implements {Blockly.ICopyable} * @constructor */ Blockly.WorkspaceCommentSvg = function(workspace, content, height, width, @@ -612,6 +616,19 @@ Blockly.WorkspaceCommentSvg.prototype.toXmlWithXY = function(opt_noId) { return element; }; +/** + * Encode a comment for copying. + * @return {!Blockly.ICopyable.CopyData} Copy metadata. + * @package + */ +Blockly.WorkspaceCommentSvg.prototype.toCopyData = function() { + return { + xml: this.toXmlWithXY(), + source: this.workspace, + typeCounts: null + }; +}; + /** * CSS for workspace comment. See css.js for use. */ @@ -625,7 +642,7 @@ Blockly.Css.register([ '.blocklyCommentRect {', 'fill: #E7DE8E;', 'stroke: #bcA903;', - 'stroke-width: 1px', + 'stroke-width: 1px;', '}', '.blocklyCommentTarget {', @@ -658,11 +675,11 @@ Blockly.Css.register([ '.blocklyCommentDeleteIcon {', 'cursor: pointer;', 'fill: #000;', - 'display: none', + 'display: none;', '}', '.blocklySelected > .blocklyCommentDeleteIcon {', - 'display: block', + 'display: block;', '}', '.blocklyDeleteIconShape {', diff --git a/core/workspace_drag_surface_svg.js b/core/workspace_drag_surface_svg.js index 0a224fc87..3ee7454be 100644 --- a/core/workspace_drag_surface_svg.js +++ b/core/workspace_drag_surface_svg.js @@ -39,16 +39,6 @@ Blockly.WorkspaceDragSurfaceSvg = function(container) { */ Blockly.WorkspaceDragSurfaceSvg.prototype.SVG_ = null; -/** - * SVG group inside the drag surface that holds blocks while a drag is in - * progress. Blocks are moved here by the workspace at start of a drag and moved - * back into the main SVG at the end of a drag. - * - * @type {Element} - * @private - */ -Blockly.WorkspaceDragSurfaceSvg.prototype.dragGroup_ = null; - /** * Containing HTML element; parent of the workspace and the drag surface. * @type {Element} diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 060ec942c..d1cfff478 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -24,20 +24,25 @@ goog.require('Blockly.MarkerManager'); goog.require('Blockly.Msg'); goog.require('Blockly.navigation'); goog.require('Blockly.Options'); +goog.require('Blockly.registry'); goog.require('Blockly.ThemeManager'); goog.require('Blockly.Themes.Classic'); goog.require('Blockly.TouchGesture'); goog.require('Blockly.utils'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.Metrics'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Rect'); +goog.require('Blockly.utils.toolbox'); goog.require('Blockly.Workspace'); goog.require('Blockly.WorkspaceAudio'); goog.require('Blockly.WorkspaceDragSurfaceSvg'); goog.require('Blockly.Xml'); goog.requireType('Blockly.blockRendering.Renderer'); +goog.requireType('Blockly.IASTNodeLocationSvg'); +goog.requireType('Blockly.IBoundedElement'); /** @@ -49,15 +54,16 @@ goog.requireType('Blockly.blockRendering.Renderer'); * @param {Blockly.WorkspaceDragSurfaceSvg=} opt_wsDragSurface Drag surface for * the workspace. * @extends {Blockly.Workspace} + * @implements {Blockly.IASTNodeLocationSvg} * @constructor */ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface) { Blockly.WorkspaceSvg.superClass_.constructor.call(this, options); - /** @type {function():!Object} */ + /** @type {function():!Blockly.utils.Metrics} */ this.getMetrics = options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_; - /** @type {function(!Object):void} */ + /** @type {function(!{x:number, y:number}):void} */ this.setMetrics = options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_; @@ -96,7 +102,7 @@ Blockly.WorkspaceSvg = function(options, * @private */ this.grid_ = this.options.gridPattern ? - new Blockly.Grid(options.gridPattern, options.gridOptions) : null; + new Blockly.Grid(this.options.gridPattern, options.gridOptions) : null; /** * Manager in charge of markers and cursors. @@ -166,6 +172,13 @@ Blockly.WorkspaceSvg = function(options, * @type {boolean} */ this.keyboardAccessibilityMode = false; + + /** + * The list of top-level bounded elements on the workspace. + * @type {!Array.} + * @private + */ + this.topBoundedElements_ = []; }; Blockly.utils.object.inherits(Blockly.WorkspaceSvg, Blockly.Workspace); @@ -329,7 +342,7 @@ Blockly.WorkspaceSvg.prototype.flyout_ = null; /** * Category-based toolbox providing blocks which may be dragged into this * workspace. - * @type {Blockly.Toolbox} + * @type {Blockly.IToolbox} * @private */ Blockly.WorkspaceSvg.prototype.toolbox_ = null; @@ -529,7 +542,7 @@ Blockly.WorkspaceSvg.prototype.refreshTheme = function() { // Update current toolbox selection. this.refreshToolboxSelection(); if (this.toolbox_) { - this.toolbox_.updateColourFromTheme(); + this.toolbox_.refreshTheme(); } // Re-render if workspace is visible @@ -737,7 +750,9 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) { if (!Blockly.Toolbox) { throw Error('Missing require for Blockly.Toolbox'); } - this.toolbox_ = new Blockly.Toolbox(this); + var ToolboxClass = Blockly.registry.getClassFromOptions( + Blockly.registry.Type.TOOLBOX, this.options); + this.toolbox_ = new ToolboxClass(this); } if (this.grid_) { this.grid_.update(this.scale); @@ -802,6 +817,14 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { this.renderer_.dispose(); + if (this.markerManager_) { + this.markerManager_.dispose(); + this.markerManager_ = null; + } + + Blockly.WorkspaceSvg.superClass_.dispose.call(this); + + // Dispose of theme manager after all blocks and mutators are disposed of. if (this.themeManager_) { this.themeManager_.unsubscribeWorkspace(this); this.themeManager_.unsubscribe(this.svgBackground_); @@ -811,13 +834,6 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { } } - if (this.markerManager_) { - this.markerManager_.dispose(); - this.markerManager_ = null; - } - - Blockly.WorkspaceSvg.superClass_.dispose.call(this); - this.connectionDBList = null; this.toolboxCategoryCallbacks_ = null; @@ -936,7 +952,7 @@ Blockly.WorkspaceSvg.prototype.getFlyout = function(opt_own) { /** * Getter for the toolbox associated with this workspace, if one exists. - * @return {Blockly.Toolbox} The toolbox on this workspace. + * @return {Blockly.IToolbox} The toolbox on this workspace. * @package */ Blockly.WorkspaceSvg.prototype.getToolbox = function() { @@ -1175,7 +1191,7 @@ Blockly.WorkspaceSvg.prototype.setVisible = function(isVisible) { this.getParentSvg().style.display = isVisible ? 'block' : 'none'; if (this.toolbox_) { // Currently does not support toolboxes in mutators. - this.toolbox_.HtmlDiv.style.display = isVisible ? 'block' : 'none'; + this.toolbox_.setVisible(isVisible); } if (isVisible) { var blocks = this.getAllBlocks(false); @@ -1289,8 +1305,9 @@ Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) { if (this.keyboardAccessibilityMode && markedNode && markedNode.isConnection()) { var markedLocation = - /** @type {!Blockly.Connection} */ (markedNode.getLocation()); - Blockly.navigation.insertBlock(block, markedLocation); + /** @type {!Blockly.RenderedConnection} */ (markedNode.getLocation()); + Blockly.navigation.insertBlock(/** @type {!Blockly.BlockSvg} */ (block), + markedLocation); return; } @@ -1442,7 +1459,7 @@ Blockly.WorkspaceSvg.prototype.recordDeleteAreas = function() { } if (this.flyout_) { this.deleteAreaToolbox_ = this.flyout_.getClientRect(); - } else if (this.toolbox_) { + } else if (this.toolbox_ && typeof this.toolbox_.getClientRect == 'function') { this.deleteAreaToolbox_ = this.toolbox_.getClientRect(); } else { this.deleteAreaToolbox_ = null; @@ -1612,9 +1629,7 @@ Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) { * bounding box containing the blocks on the workspace. */ Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() { - var topBlocks = this.getTopBlocks(false); - var topComments = this.getTopComments(false); - var topElements = topBlocks.concat(topComments); + var topElements = this.getTopBoundedElements(); // There are no blocks, return empty rectangle. if (!topElements.length) { return new Blockly.utils.Rect(0, 0, 0, 0); @@ -1815,11 +1830,16 @@ Blockly.WorkspaceSvg.prototype.showContextMenu = function(e) { /** * Modify the block tree on the existing toolbox. - * @param {Node|string} tree DOM tree of blocks, or text representation of same. + * @param {Blockly.utils.toolbox.ToolboxDefinition|string} toolboxDef + * DOM tree of toolbox contents, string of toolbox contents, or array of JSON + * representing toolbox contents. */ -Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) { - tree = Blockly.Options.parseToolboxTree(tree); - if (!tree) { +Blockly.WorkspaceSvg.prototype.updateToolbox = function(toolboxDef) { + if (!Array.isArray(toolboxDef)) { + toolboxDef = Blockly.Options.parseToolboxTree(toolboxDef); + } + toolboxDef = Blockly.utils.toolbox.convertToolboxToJSON(toolboxDef); + if (!toolboxDef) { if (this.options.languageTree) { throw Error('Can\'t nullify an existing toolbox.'); } @@ -1828,18 +1848,18 @@ Blockly.WorkspaceSvg.prototype.updateToolbox = function(tree) { if (!this.options.languageTree) { throw Error('Existing toolbox is null. Can\'t create new toolbox.'); } - if (tree.getElementsByTagName('category').length) { + if (Blockly.utils.toolbox.hasCategories(toolboxDef)) { if (!this.toolbox_) { throw Error('Existing toolbox has no categories. Can\'t change mode.'); } - this.options.languageTree = tree; - this.toolbox_.renderTree(tree); + this.options.languageTree = toolboxDef; + this.toolbox_.render(toolboxDef); } else { if (!this.flyout_) { throw Error('Existing toolbox has categories. Can\'t change mode.'); } - this.options.languageTree = tree; - this.flyout_.show(tree.childNodes); + this.options.languageTree = toolboxDef; + this.flyout_.show(toolboxDef); } }; @@ -1952,8 +1972,8 @@ Blockly.WorkspaceSvg.prototype.zoomCenter = function(type) { // when the size of the flyout increases) you need the center of the // *blockly div* to stay in the same pixel-position. // Note: This only works because of how scrollCenter positions blocks. - var x = metrics.svgWidth / 2; - var y = metrics.svgHeight / 2; + var x = metrics.svgWidth ? metrics.svgWidth / 2 : 0; + var y = metrics.svgHeight ? metrics.svgHeight / 2 : 0; } else { var x = (metrics.viewWidth / 2) + metrics.absoluteLeft; var y = (metrics.viewHeight / 2) + metrics.absoluteTop; @@ -2202,11 +2222,10 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { // the content's top-left to the view's top-left, matching the // directionality of the scrollbars. - // TODO (#2299): Change these to not use the internal ratio_ property. this.scrollbar.hScroll.setHandlePosition(-(x + metrics.contentLeft) * - this.scrollbar.hScroll.ratio_); + this.scrollbar.hScroll.ratio); this.scrollbar.vScroll.setHandlePosition(-(y + metrics.contentTop) * - this.scrollbar.vScroll.ratio_); + this.scrollbar.vScroll.ratio); } // We have to shift the translation so that when the canvas is at 0, 0 the // workspace origin is not underneath the toolbox. @@ -2217,11 +2236,11 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { /** * Get the dimensions of the given workspace component, in pixels. - * @param {Blockly.Toolbox|Blockly.Flyout} elem The element to get the + * @param {Blockly.IToolbox|Blockly.Flyout} elem The element to get the * dimensions of, or null. It should be a toolbox or flyout, and should * implement getWidth() and getHeight(). - * @return {!Object} An object containing width and height attributes, which - * will both be zero if elem did not exist. + * @return {!Blockly.utils.Size} An object containing width and height + * attributes, which will both be zero if elem did not exist. * @private */ Blockly.WorkspaceSvg.getDimensionsPx_ = function(elem) { @@ -2231,10 +2250,7 @@ Blockly.WorkspaceSvg.getDimensionsPx_ = function(elem) { width = elem.getWidth(); height = elem.getHeight(); } - return { - width: width, - height: height - }; + return new Blockly.utils.Size(width, height); }; /** @@ -2355,8 +2371,8 @@ Blockly.WorkspaceSvg.getContentDimensionsBounded_ = function(ws, svgSize) { * .flyoutHeight: Height of the flyout if it is always open. Otherwise zero. * .toolboxPosition: Top, bottom, left or right. Use TOOLBOX_AT constants to * compare. - * @return {!Object} Contains size and position metrics of a top level - * workspace. + * @return {!Blockly.utils.Metrics} Contains size and position metrics of a top + * level workspace. * @private * @this {Blockly.WorkspaceSvg} */ @@ -2426,11 +2442,10 @@ Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() { toolboxWidth: toolboxDimensions.width, toolboxHeight: toolboxDimensions.height, + toolboxPosition: this.toolboxPosition, flyoutWidth: flyoutDimensions.width, - flyoutHeight: flyoutDimensions.height, - - toolboxPosition: this.toolboxPosition + flyoutHeight: flyoutDimensions.height }; return metrics; }; @@ -2480,6 +2495,68 @@ Blockly.WorkspaceSvg.prototype.getTopBlocks = function(ordered) { return Blockly.WorkspaceSvg.superClass_.getTopBlocks.call(this, ordered); }; +/** + * Adds a block to the list of top blocks. + * @param {!Blockly.Block} block Block to add. + */ +Blockly.WorkspaceSvg.prototype.addTopBlock = function(block) { + this.addTopBoundedElement(/** @type {!Blockly.BlockSvg} */ (block)); + Blockly.WorkspaceSvg.superClass_.addTopBlock.call(this, block); +}; + +/** + * Removes a block from the list of top blocks. + * @param {!Blockly.Block} block Block to remove. + */ +Blockly.WorkspaceSvg.prototype.removeTopBlock = function(block) { + this.removeTopBoundedElement(/** @type {!Blockly.BlockSvg} */ (block)); + Blockly.WorkspaceSvg.superClass_.removeTopBlock.call(this, block); +}; + +/** + * Adds a comment to the list of top comments. + * @param {!Blockly.WorkspaceComment} comment comment to add. + */ +Blockly.WorkspaceSvg.prototype.addTopComment = function(comment) { + this.addTopBoundedElement( + /** @type {!Blockly.WorkspaceCommentSvg} */ (comment)); + Blockly.WorkspaceSvg.superClass_.addTopComment.call(this, comment); +}; + +/** + * Removes a comment from the list of top comments. + * @param {!Blockly.WorkspaceComment} comment comment to remove. + */ +Blockly.WorkspaceSvg.prototype.removeTopComment = function(comment) { + this.removeTopBoundedElement( + /** @type {!Blockly.WorkspaceCommentSvg} */ (comment)); + Blockly.WorkspaceSvg.superClass_.removeTopComment.call(this, comment); +}; + +/** + * Adds a bounded element to the list of top bounded elements. + * @param {!Blockly.IBoundedElement} element Bounded element to add. + */ +Blockly.WorkspaceSvg.prototype.addTopBoundedElement = function(element) { + this.topBoundedElements_.push(element); +}; + +/** + * Removes a bounded element from the list of top bounded elements. + * @param {!Blockly.IBoundedElement} element Bounded element to remove. + */ +Blockly.WorkspaceSvg.prototype.removeTopBoundedElement = function(element) { + Blockly.utils.arrayRemove(this.topBoundedElements_, element); +}; + +/** + * Finds the top-level bounded elements and returns them. + * @return {!Array.} The top-level bounded elements. + */ +Blockly.WorkspaceSvg.prototype.getTopBoundedElements = function() { + return [].concat(this.topBoundedElements_); +}; + /** * Update whether this workspace has resizes enabled. * If enabled, workspace will resize when appropriate. @@ -2502,6 +2579,7 @@ Blockly.WorkspaceSvg.prototype.setResizesEnabled = function(enabled) { Blockly.WorkspaceSvg.prototype.clear = function() { this.setResizesEnabled(false); Blockly.WorkspaceSvg.superClass_.clear.call(this); + this.topBoundedElements_ = []; this.setResizesEnabled(true); }; diff --git a/core/xml.js b/core/xml.js index e5d7cd6e3..45b3e0ca6 100644 --- a/core/xml.js +++ b/core/xml.js @@ -77,10 +77,20 @@ Blockly.Xml.variablesToDom = function(variableList) { * @return {!Element} Tree of XML elements. */ Blockly.Xml.blockToDomWithXY = function(block, opt_noId) { + if (block.isInsertionMarker()) { // Skip over insertion markers. + block = block.getChildren(false)[0]; + if (!block) { + // Disappears when appended. Cast to ANY b/c DocumentFragment -> Element + // is invalid. We have to cast to ANY in between. + return /** @type{?} */ (new DocumentFragment()); + } + } + var width; // Not used in LTR. if (block.workspace.RTL) { width = block.workspace.getWidth(); } + var element = Blockly.Xml.blockToDom(block, opt_noId); var xy = block.getRelativeToSurfaceXY(); element.setAttribute('x', @@ -131,6 +141,19 @@ Blockly.Xml.allFieldsToDom_ = function(block, element) { * @return {!Element} Tree of XML elements. */ Blockly.Xml.blockToDom = function(block, opt_noId) { + // Skip over insertion markers. + if (block.isInsertionMarker()) { + var child = block.getChildren(false)[0]; + if (child) { + return Blockly.Xml.blockToDom(child); + } else { + // Disappears when appended. Cast to ANY b/c DocumentFragment -> Element + // is invalid. We have to cast to ANY in between. + return /** @type{?} */ (new DocumentFragment()); + } + } + + var element = Blockly.utils.xml.createElement(block.isShadow() ? 'shadow' : 'block'); element.setAttribute('type', block.type); @@ -186,8 +209,11 @@ Blockly.Xml.blockToDom = function(block, opt_noId) { container.appendChild(Blockly.Xml.cloneShadow_(shadow, opt_noId)); } if (childBlock) { - container.appendChild(Blockly.Xml.blockToDom(childBlock, opt_noId)); - empty = false; + var elem = Blockly.Xml.blockToDom(childBlock, opt_noId); + if (elem.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) { + container.appendChild(elem); + empty = false; + } } } container.setAttribute('name', input.name); @@ -217,9 +243,12 @@ Blockly.Xml.blockToDom = function(block, opt_noId) { var nextBlock = block.getNextBlock(); if (nextBlock) { - var container = Blockly.utils.xml.createElement('next'); - container.appendChild(Blockly.Xml.blockToDom(nextBlock, opt_noId)); - element.appendChild(container); + var elem = Blockly.Xml.blockToDom(nextBlock, opt_noId); + if (elem.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) { + var container = Blockly.utils.xml.createElement('next'); + container.appendChild(elem); + element.appendChild(container); + } } var shadow = block.nextConnection && block.nextConnection.getShadowDom(); if (shadow && (!nextBlock || !nextBlock.isShadow())) { @@ -253,7 +282,7 @@ Blockly.Xml.cloneShadow_ = function(shadow, opt_noId) { while (node && !node.nextSibling) { textNode = node; node = node.parentNode; - if (textNode.nodeType == Blockly.utils.dom.Node.TEXT_NODE && + if (textNode.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE && textNode.data.trim() == '' && node.firstChild != textNode) { // Prune whitespace after a tag. Blockly.utils.dom.removeNode(textNode); @@ -262,7 +291,8 @@ Blockly.Xml.cloneShadow_ = function(shadow, opt_noId) { if (node) { textNode = node; node = node.nextSibling; - if (textNode.nodeType == Blockly.utils.dom.Node.TEXT_NODE && textNode.data.trim() == '') { + if (textNode.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE && + textNode.data.trim() == '') { // Prune whitespace before a tag. Blockly.utils.dom.removeNode(textNode); } @@ -583,7 +613,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { */ Blockly.Xml.domToVariables = function(xmlVariables, workspace) { for (var i = 0, xmlChild; (xmlChild = xmlVariables.childNodes[i]); i++) { - if (xmlChild.nodeType != Blockly.utils.dom.Node.ELEMENT_NODE) { + if (xmlChild.nodeType != Blockly.utils.dom.NodeType.ELEMENT_NODE) { continue; // Skip text nodes. } var type = xmlChild.getAttribute('type'); @@ -613,7 +643,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { var blockChild = null; for (var i = 0, xmlChild; (xmlChild = xmlBlock.childNodes[i]); i++) { - if (xmlChild.nodeType == Blockly.utils.dom.Node.TEXT_NODE) { + if (xmlChild.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE) { // Ignore any text at the level. It's all whitespace anyway. continue; } @@ -623,7 +653,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { var childBlockElement = null; var childShadowElement = null; for (var j = 0, grandchild; (grandchild = xmlChild.childNodes[j]); j++) { - if (grandchild.nodeType == Blockly.utils.dom.Node.ELEMENT_NODE) { + if (grandchild.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) { if (grandchild.nodeName.toLowerCase() == 'block') { childBlockElement = /** @type {!Element} */ (grandchild); } else if (grandchild.nodeName.toLowerCase() == 'shadow') { diff --git a/dart_compressed.js b/dart_compressed.js index 264e3de5d..d13cfeb8d 100644 --- a/dart_compressed.js +++ b/dart_compressed.js @@ -1,8 +1,16 @@ // Do not edit this file; automatically generated by gulp. -'use strict'; - -Blockly.Dart=new Blockly.Generator("Dart");Blockly.Dart.addReservedWords("assert,break,case,catch,class,const,continue,default,do,else,enum,extends,false,final,finally,for,if,in,is,new,null,rethrow,return,super,switch,this,throw,true,try,var,void,while,with,print,identityHashCode,identical,BidirectionalIterator,Comparable,double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,ArgumentError,AssertionError,CastError,ConcurrentModificationError,CyclicInitializationError,Error,Exception,FallThroughError,FormatException,IntegerDivisionByZeroException,NoSuchMethodError,NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,StateError,TypeError,UnimplementedError,UnsupportedError"); +/* eslint-disable */ +;(function(root, factory) { + if (typeof define === 'function' && define.amd) { // AMD + define(['./blockly_compressed.js'], factory); + } else if (typeof exports === 'object') { // Node.js + module.exports = factory(require('./blockly_compressed.js')); + } else { // Browser + root.Blockly.Dart = factory(root.Blockly); + } +}(this, function(Blockly) { + 'use strict';Blockly.Dart=new Blockly.Generator("Dart");Blockly.Dart.addReservedWords("assert,break,case,catch,class,const,continue,default,do,else,enum,extends,false,final,finally,for,if,in,is,new,null,rethrow,return,super,switch,this,throw,true,try,var,void,while,with,print,identityHashCode,identical,BidirectionalIterator,Comparable,double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,ArgumentError,AssertionError,CastError,ConcurrentModificationError,CyclicInitializationError,Error,Exception,FallThroughError,FormatException,IntegerDivisionByZeroException,NoSuchMethodError,NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,StateError,TypeError,UnimplementedError,UnsupportedError"); Blockly.Dart.ORDER_ATOMIC=0;Blockly.Dart.ORDER_UNARY_POSTFIX=1;Blockly.Dart.ORDER_UNARY_PREFIX=2;Blockly.Dart.ORDER_MULTIPLICATIVE=3;Blockly.Dart.ORDER_ADDITIVE=4;Blockly.Dart.ORDER_SHIFT=5;Blockly.Dart.ORDER_BITWISE_AND=6;Blockly.Dart.ORDER_BITWISE_XOR=7;Blockly.Dart.ORDER_BITWISE_OR=8;Blockly.Dart.ORDER_RELATIONAL=9;Blockly.Dart.ORDER_EQUALITY=10;Blockly.Dart.ORDER_LOGICAL_AND=11;Blockly.Dart.ORDER_LOGICAL_OR=12;Blockly.Dart.ORDER_IF_NULL=13;Blockly.Dart.ORDER_CONDITIONAL=14; Blockly.Dart.ORDER_CASCADE=15;Blockly.Dart.ORDER_ASSIGNMENT=16;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_);Blockly.Dart.variableDB_.setVariableMap(a.getVariableMap());for(var b=[],c=Blockly.Variables.allDeveloperVariables(a),d=0;d - diff --git a/demos/blockfactory_old/blocks.js b/demos/blockfactory_old/blocks.js index 050e53147..467eca2af 100644 --- a/demos/blockfactory_old/blocks.js +++ b/demos/blockfactory_old/blocks.js @@ -460,24 +460,6 @@ Blockly.Blocks['field_colour'] = { } }; -Blockly.Blocks['field_date'] = { - // Date input. - init: function() { - this.setColour(160); - this.appendDummyInput() - .appendField('date') - .appendField(new Blockly.FieldDate(), 'DATE') - .appendField(',') - .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.setPreviousStatement(true, 'Field'); - this.setNextStatement(true, 'Field'); - this.setTooltip('Date input field.'); - }, - onchange: function() { - fieldNameCheck(this); - } -}; - Blockly.Blocks['field_variable'] = { // Dropdown for variables. init: function() { diff --git a/demos/blockfactory_old/factory.js b/demos/blockfactory_old/factory.js index 967d2a440..10c0adad1 100644 --- a/demos/blockfactory_old/factory.js +++ b/demos/blockfactory_old/factory.js @@ -335,12 +335,6 @@ function getFieldsJs_(block) { escapeString(block.getFieldValue('COLOUR')) + '), ' + escapeString(block.getFieldValue('FIELDNAME'))); break; - case 'field_date': - // Result: new Blockly.FieldDate('2015-02-04'), 'DATE' - fields.push('new Blockly.FieldDate(' + - escapeString(block.getFieldValue('DATE')) + '), ' + - escapeString(block.getFieldValue('FIELDNAME'))); - break; case 'field_variable': // Result: new Blockly.FieldVariable('item'), 'VAR' var varname = escapeString(block.getFieldValue('TEXT') || null); @@ -440,13 +434,6 @@ function getFieldsJson_(block) { colour: block.getFieldValue('COLOUR') }); break; - case 'field_date': - fields.push({ - type: block.type, - name: block.getFieldValue('FIELDNAME'), - date: block.getFieldValue('DATE') - }); - break; case 'field_variable': fields.push({ type: block.type, @@ -577,10 +564,6 @@ function updateGenerator(block) { // Subclass of Blockly.FieldTextInput, must test first. code.push(makeVar('angle', name) + " = block.getFieldValue('" + name + "');"); - } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) { - // Blockly.FieldDate may not be compiled into Blockly. - code.push(makeVar('date', name) + - " = block.getFieldValue('" + name + "');"); } else if (field instanceof Blockly.FieldColour) { code.push(makeVar('colour', name) + " = block.getFieldValue('" + name + "');"); diff --git a/demos/blockfactory_old/index.html b/demos/blockfactory_old/index.html index b03db9127..7cc448f81 100644 --- a/demos/blockfactory_old/index.html +++ b/demos/blockfactory_old/index.html @@ -196,11 +196,6 @@ - diff --git a/demos/custom-fields/pitch/field_pitch.js b/demos/custom-fields/pitch/field_pitch.js index b66dca6a8..5b8ef5e1a 100644 --- a/demos/custom-fields/pitch/field_pitch.js +++ b/demos/custom-fields/pitch/field_pitch.js @@ -63,7 +63,7 @@ CustomFields.FieldPitch.NOTES = 'C3 D3 E3 F3 G3 A3 B3 C4 D4 E4 F4 G4 A4'.split(/ /** * Show the inline free-text editor on top of the text and the note picker. - * @private + * @protected */ CustomFields.FieldPitch.prototype.showEditor_ = function() { CustomFields.FieldPitch.superClass_.showEditor_.call(this); diff --git a/demos/index.html b/demos/index.html index 1b22326a3..d97b89c51 100644 --- a/demos/index.html +++ b/demos/index.html @@ -181,7 +181,7 @@ -
Two Blockly instances connected as master-slave.
+
Two Blockly instances connected as leader-follower.
diff --git a/demos/minimap/minimap.js b/demos/minimap/minimap.js index db467a503..fe19065bf 100644 --- a/demos/minimap/minimap.js +++ b/demos/minimap/minimap.js @@ -35,7 +35,7 @@ Minimap.init = function(workspace, minimap) { // New code starts from here. // Get the absolutePosition. - var absolutePosition = (this.handlePosition_ / this.ratio_); + var absolutePosition = (this.handlePosition_ / this.ratio); // Firing the scroll change listener. Minimap.onScrollChange(absolutePosition, this.horizontal_); @@ -50,7 +50,7 @@ Minimap.init = function(workspace, minimap) { // New code starts from here. // Get the absolutePosition. - var absolutePosition = (this.handlePosition_ / this.ratio_); + var absolutePosition = (this.handlePosition_ / this.ratio); // Firing the scroll change listener. Minimap.onScrollChange(absolutePosition, this.horizontal_); diff --git a/demos/plane/soy/soyutils.js b/demos/plane/soy/soyutils.js index 949777cb9..e18764c91 100644 --- a/demos/plane/soy/soyutils.js +++ b/demos/plane/soy/soyutils.js @@ -2002,7 +2002,7 @@ soy.$$cleanHtml = function(value) { return /** @type {!soydata.SanitizedHtml} */ (value); } return soydata.VERY_UNSAFE.ordainSanitizedHtml( - soy.$$stripHtmlTags(value, soy.esc.$$SAFE_TAG_WHITELIST_), + soy.$$stripHtmlTags(value, soy.esc.$$SAFE_TAG_ALLOWLIST_), soydata.getContentDir(value)); }; @@ -2047,20 +2047,20 @@ soy.$$HTML5_VOID_ELEMENTS_ = new RegExp( /** * Removes HTML tags from a string of known safe HTML. - * If opt_tagWhitelist is not specified or is empty, then + * If opt_tagAllowlist is not specified or is empty, then * the result can be used as an attribute value. * * @param {*} value The HTML to be escaped. May not be a string, but the * value will be coerced to a string. - * @param {Object.=} opt_tagWhitelist Has an own property whose + * @param {Object.=} opt_tagAllowlist Has an own property whose * name is a lower-case tag name and whose value is {@code 1} for * each element that is allowed in the output. * @return {string} A representation of value without disallowed tags, * HTML comments, or other non-text content. */ -soy.$$stripHtmlTags = function(value, opt_tagWhitelist) { - if (!opt_tagWhitelist) { - // If we have no white-list, then use a fast track which elides all tags. +soy.$$stripHtmlTags = function(value, opt_tagAllowlist) { + if (!opt_tagAllowlist) { + // If we have no allow-list, then use a fast track which elides all tags. return String(value).replace(soy.esc.$$HTML_TAG_REGEX_, '') // This is just paranoia since callers should normalize the result // anyway, but if they didn't, it would be necessary to ensure that @@ -2073,7 +2073,7 @@ soy.$$stripHtmlTags = function(value, opt_tagWhitelist) { // have been removed. var html = String(value).replace(/\[/g, '['); - // Consider all uses of '<' and replace whitelisted tags with markers like + // Consider all uses of '<' and replace allowlisted tags with markers like // [1] which are indices into a list of approved tag names. // Replace all other uses of < and > with entities. var tags = []; @@ -2082,8 +2082,8 @@ soy.$$stripHtmlTags = function(value, opt_tagWhitelist) { function(tok, tagName) { if (tagName) { tagName = tagName.toLowerCase(); - if (opt_tagWhitelist.hasOwnProperty(tagName) && - opt_tagWhitelist[tagName]) { + if (opt_tagAllowlist.hasOwnProperty(tagName) && + opt_tagAllowlist[tagName]) { var start = tok.charAt(1) === '/' ? ''; @@ -2102,7 +2102,7 @@ soy.$$stripHtmlTags = function(value, opt_tagWhitelist) { // Now html contains no tags or less-than characters that could become // part of a tag via a replacement operation and tags only contains // approved tags. - // Reinsert the white-listed tags. + // Reinsert the allow-listed tags. html = html.replace( /\[(\d+)\]/g, function(_, index) { return tags[index]; }); @@ -3294,6 +3294,6 @@ soy.esc.$$LT_REGEX_ = /} * @private */ -soy.esc.$$SAFE_TAG_WHITELIST_ = {'b': 1, 'br': 1, 'em': 1, 'i': 1, 's': 1, 'sub': 1, 'sup': 1, 'u': 1}; +soy.esc.$$SAFE_TAG_ALLOWLIST_ = {'b': 1, 'br': 1, 'em': 1, 'i': 1, 's': 1, 'sub': 1, 'sup': 1, 'u': 1}; // END GENERATED CODE diff --git a/demos/storage/index.html b/demos/storage/index.html index 41462e295..38877a092 100644 --- a/demos/storage/index.html +++ b/demos/storage/index.html @@ -32,7 +32,7 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/blocks/logic_ternary_test.js b/tests/blocks/logic_ternary_test.js deleted file mode 100644 index 81142c349..000000000 --- a/tests/blocks/logic_ternary_test.js +++ /dev/null @@ -1,302 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -function test_logic_ternary_structure() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - assertEquals(3, block.inputList && block.inputList.length); - assertEquals(1, block.getInput('IF').connection.check_.length); - assertEquals('Boolean', block.getInput('IF').connection.check_[0]); - assertTrue(!!block.onchangeWrapper_); // Has onchange handler - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachSameTypeCheckInThenAndElseWithoutParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - - var string1 = workspace.newBlock('text'); - var string2 = workspace.newBlock('text_charAt'); - - block.getInput('THEN').connection.connect(string1.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string1.getRootBlock()); - block.getInput('ELSE').connection.connect(string2.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string1.getRootBlock()); // Still connected. - assertEquals(block, string2.getRootBlock()); - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachDifferectTypeChecksInThenAndElseWithoutParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - - var string = workspace.newBlock('text'); - var number = workspace.newBlock('math_number'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); - block.getInput('ELSE').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); // Input THEN still connected. - assertEquals(block, number.getRootBlock()); - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachSameTypeCheckInThenAndElseWithMatchingParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var parent = workspace.newBlock('text_trim'); - - parent.getInput('TEXT').connection.connect(block.outputConnection); - assertEquals(parent, block.getRootBlock()); - - var string1 = workspace.newBlock('text'); - var string2 = workspace.newBlock('text_charAt'); - - block.getInput('THEN').connection.connect(string1.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string1.getRootBlock()); - block.getInput('ELSE').connection.connect(string2.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string1.getRootBlock()); // Input THEN still connected. - assertEquals(parent, string2.getRootBlock()); - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachDifferectTypeChecksInThenAndElseWithUncheckedParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var parent = workspace.newBlock('text_print'); - - parent.getInput('TEXT').connection.connect(block.outputConnection); - assertEquals(parent, block.parentBlock_); - - var string = workspace.newBlock('text'); - var number = workspace.newBlock('math_number'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string.getRootBlock()); - block.getInput('ELSE').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string.getRootBlock()); // Input THEN still connected. - assertEquals(parent, number.getRootBlock()); - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachDifferectTypeChecksInThenAndElseWithPermissiveParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var parent = workspace.newBlock('text_length'); // Allows String or Array - - parent.getInput('VALUE').connection.connect(block.outputConnection); - assertEquals(parent, block.parentBlock_); - - var string = workspace.newBlock('text'); - var array = workspace.newBlock('lists_create_empty'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string.getRootBlock()); - block.getInput('ELSE').connection.connect(array.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string.getRootBlock()); // Input THEN still connected. - assertEquals(parent, array.getRootBlock()); - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachMismatchTypeToThen_breakWithParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var parent = workspace.newBlock('text_length'); // Allows String or Array - - parent.getInput('VALUE').connection.connect(block.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.parentBlock_); - - var string = workspace.newBlock('text'); - var number = workspace.newBlock('math_number'); - - block.getInput('ELSE').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string.getRootBlock()); - - // Adding mismatching number. - block.getInput('THEN').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, block.getRootBlock()); // Disconnected from parent. - assertEquals(block, number.getRootBlock()); - assertEquals(block, string.getRootBlock()); // ELSE string still connected. - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachMismatchTypeToElse_breakWithParent() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var parent = workspace.newBlock('text_length'); // Allows String or Array - - parent.getInput('VALUE').connection.connect(block.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.parentBlock_); - - var string = workspace.newBlock('text'); - var number = workspace.newBlock('math_number'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Still connected to parent. - assertEquals(parent, string.getRootBlock()); - - // Adding mismatching number. - block.getInput('ELSE').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, block.getRootBlock()); // Disconnected from parent. - assertEquals(block, number.getRootBlock()); - assertEquals(block, string.getRootBlock()); // THEN string still connected. - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachToUncheckedParentWithDifferentTypes() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var string = workspace.newBlock('text'); - var number = workspace.newBlock('math_number'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); - block.getInput('ELSE').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); // Input THEN still connected. - assertEquals(block, number.getRootBlock()); - - // Attaching to parent. - var parent = workspace.newBlock('text_print'); - parent.getInput('TEXT').connection.connect(block.outputConnection); - assertEquals(parent, block.getRootBlock()); - assertEquals(parent, string.getRootBlock()); // Input THEN still connected. - assertEquals(parent, number.getRootBlock()); // Input ELSE still connected. - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachToPermissiveParentWithDifferentTypes() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var string = workspace.newBlock('text'); - var array = workspace.newBlock('lists_create_empty'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); - block.getInput('ELSE').connection.connect(array.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); // Input THEN still connected. - assertEquals(block, array.getRootBlock()); - - // Attaching to parent. - var parent = workspace.newBlock('text_print'); - parent.getInput('TEXT').connection.connect(block.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); - assertEquals(parent, string.getRootBlock()); // Input THEN still connected. - assertEquals(parent, array.getRootBlock()); // Input ELSE still connected. - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachToParentWithMismatchingThen_disconnectThen() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var number = workspace.newBlock('math_number'); - var string = workspace.newBlock('text'); - - block.getInput('THEN').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, number.getRootBlock()); - block.getInput('ELSE').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, number.getRootBlock()); // Input THEN still connected. - assertEquals(block, string.getRootBlock()); - - // Attaching to parent. - var parent = workspace.newBlock('text_trim'); - parent.getInput('TEXT').connection.connect(block.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Successful connection to parent. - assertEquals(parent, string.getRootBlock()); // Input ELSE still connected. - assertEquals(number, number.getRootBlock()); // Input THEN disconnected. - } finally { - workspace.dispose(); - } -} - -function test_logic_ternary_attachToParentWithMismatchingElse_disconnectElse() { - var workspace = new Blockly.Workspace(); - try { - var block = workspace.newBlock('logic_ternary'); - var string = workspace.newBlock('text'); - var number = workspace.newBlock('math_number'); - - block.getInput('THEN').connection.connect(string.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); - block.getInput('ELSE').connection.connect(number.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(block, string.getRootBlock()); // Input THEN still connected. - assertEquals(block, number.getRootBlock()); - - // Attaching to parent. - var parent = workspace.newBlock('text_trim'); - parent.getInput('TEXT').connection.connect(block.outputConnection); - Blockly.Events.fireNow_(); // Force synchronous onchange() call. - assertEquals(parent, block.getRootBlock()); // Successful connection to parent. - assertEquals(parent, string.getRootBlock()); // Input THEN still connected. - assertEquals(number, number.getRootBlock()); // Input ELSE disconnected. - } finally { - workspace.dispose(); - } -} diff --git a/tests/blocks/test_blocks.js b/tests/blocks/test_blocks.js index 47b44317d..524a89f90 100644 --- a/tests/blocks/test_blocks.js +++ b/tests/blocks/test_blocks.js @@ -657,6 +657,25 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT } ] }, + { + "type": "test_dropdowns_in_mutator", + "message0": "dropdown mutator", + "mutator": "test_dropdown_mutator" + }, + { + "type": "test_dropdowns_in_mutator_block", + "message0": "dropdown %1", + "args0": [ + { + "type": "field_dropdown", + "name": "DROPDOWN", + "options": [ + [ "option", "ONE" ], + [ "option", "TWO" ] + ] + }, + ] + }, { "type": "test_fields_angle", "message0": "angle: %1", @@ -674,23 +693,6 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT ], "style": "math_blocks", }, - { - "type": "test_fields_date", - "message0": "date: %1", - "args0": [ - { - "type": "field_date", - "name": "FIELDNAME", - "date": "2020-02-20", - "alt": - { - "type": "field_label", - "text": "NO DATE FIELD" - } - } - ], - "style": "math_blocks", - }, { "type": "test_fields_text_input", "message0": "text input %1", @@ -1209,6 +1211,24 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT ], "style": "text_blocks" }, + { + "type": "test_mutators_noflyout", + "message0": "noflyout mutator", + "mutator": "test_noflyout_mutator", + "colour": "#000000" + }, + { + "type": "test_mutators_noflyout_block", + "message0": "colour %1", + "args0": [ + { + "type": "field_colour", + "name": "COLOUR", + "colour": "#ff0000" + } + ], + "style": "colour_blocks" + }, { "type": "test_style_hat", "message0": "Hat block (event)", @@ -1343,6 +1363,22 @@ Blockly.Blocks['test_images_clickhandler'] = { } }; +Blockly.Blocks['test_validators_dispose_block'] = { + init: function() { + this.appendDummyInput() + .appendField("dispose block") + .appendField(new Blockly.FieldTextInput("default", this.validate), "INPUT"); + this.setColour(230); + this.setCommentText('Any changes to the text cause the block to be disposed'); + }, + + validate: function(newValue) { + if (newValue != "default") { + this.getSourceBlock().dispose(true); + } + } +}; + Blockly.Blocks['test_validators_text_null'] = { init: function() { this.appendDummyInput() @@ -1717,6 +1753,62 @@ Blockly.Blocks['test_basic_empty_with_mutator'] = { } }; +/** + * Mutator methods added to the test_mutators_noflyout block. + * @mixin + * @augments Blockly.Block + * @package + * @readonly + */ +var NO_FLYOUT_MUTATOR = { + /** + * Create XML to represent the block mutation. + * @return {Element} XML storage element. + * @this {Blockly.Block} + */ + mutationToDom: function() { + var container = Blockly.utils.xml.createElement('mutation'); + container.setAttribute('colour', this.colour_); + this.setColour(this.colour_); + return container; + }, + /** + * Restore a block from XML. + * @param {!Element} xmlElement XML storage element. + * @this {Blockly.Block} + */ + domToMutation: function(xmlElement) { + this.colour_ = xmlElement.getAttribute('colour'); + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this {Blockly.Block} + */ + decompose: function(workspace) { + var containerBlock = workspace.newBlock('test_mutators_noflyout_block'); + containerBlock.getField('COLOUR').setValue(this.colour_); + containerBlock.initSvg(); + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} containerBlock Root block in mutator. + * @this {Blockly.Block} + */ + compose: function(containerBlock) { + this.colour_ = containerBlock.getFieldValue('COLOUR'); + this.setColour(this.colour_); + }, +}; + +/** + * Register custom mutator used by the test_mutators_noflyout block. + */ +Blockly.Extensions.registerMutator('test_noflyout_mutator', + NO_FLYOUT_MUTATOR, null, []); + Blockly.Blocks['test_dropdowns_dynamic'] = { init: function() { var dropdown = new Blockly.FieldDropdown(this.dynamicOptions); @@ -1735,7 +1827,7 @@ Blockly.Blocks['test_dropdowns_dynamic'] = { /** * An array of options for the dynamic dropdown. - * @type {!Array} + * @type {!Array.} * @private */ Blockly.TestBlocks.dynamicDropdownOptions_ = []; @@ -1802,6 +1894,57 @@ Blockly.Blocks['test_dropdowns_dynamic_random'] = { } }; +/** + * Mutator methods added to the test_dropdowns_in_mutator block. + * @mixin + * @augments Blockly.Block + * @package + * @readonly + */ +var DROPDOWN_MUTATOR = { + /** + * Create XML to represent the block mutation. + * @return {Element} XML storage element. + * @this {Blockly.Block} + */ + mutationToDom: function() { + var container = Blockly.utils.xml.createElement('mutation'); + return container; + }, + /** + * Restore a block from XML. + * @param {!Element} _xmlElement XML storage element. + * @this {Blockly.Block} + */ + domToMutation: function(_xmlElement) { + }, + /** + * Populate the mutator's dialog with this block's components. + * @param {!Blockly.Workspace} workspace Mutator's workspace. + * @return {!Blockly.Block} Root block in mutator. + * @this {Blockly.Block} + */ + decompose: function(workspace) { + var containerBlock = workspace.newBlock('test_dropdowns_in_mutator_block'); + containerBlock.initSvg(); + + return containerBlock; + }, + /** + * Reconfigure this block based on the mutator dialog's components. + * @param {!Blockly.Block} _containerBlock Root block in mutator. + * @this {Blockly.Block} + */ + compose: function(_containerBlock) { + }, +}; + +/** + * Register custom mutator used by the test_dropdowns_in_mutator block. + */ +Blockly.Extensions.registerMutator('test_dropdown_mutator', + DROPDOWN_MUTATOR, null, ['test_dropdowns_in_mutator_block']); + /** * Handles "insert" button in the connection row test category. This will insert * a group of test blocks connected in a row. diff --git a/tests/compile/compile.sh b/tests/compile/compile.sh index b3251fc48..232ada8c7 100755 --- a/tests/compile/compile.sh +++ b/tests/compile/compile.sh @@ -75,6 +75,7 @@ done echo "Compiling Blockly..." COMPILATION_COMMAND="java -jar $COMPILER --js='$BLOCKLY_ROOT/tests/compile/main.js' \ --js='$tempPath/**.js' \ + --js='$BLOCKLY_ROOT/tests/blocks/**.js' \ --js='$BLOCKLY_ROOT/blocks/**.js' \ --js='$BLOCKLY_ROOT/generators/**.js' \ --generate_exports \ diff --git a/tests/jsunit/block_test.js b/tests/jsunit/block_test.js deleted file mode 100644 index 3dff5d20e..000000000 --- a/tests/jsunit/block_test.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - - /** - * @fileoverview Tests for Blockly.Block - * @author fenichel@google.com (Rachel Fenichel) - */ - -'use strict'; - -var workspace; -var mockControl_; - -function defineTestBlocks() { - Blockly.defineBlocksWithJsonArray([{ - "type": "stack_block", - "message0": "", - "previousStatement": null, - "nextStatement": null - }, - { - "type": "row_block", - "message0": "%1", - "args0": [ - { - "type": "input_value", - "name": "INPUT" - } - ], - "output": null - }]); -} - -function undefineTestBlocks() { - delete Blockly.Blocks['stack_block']; - delete Blockly.Blocks['row_block']; -} - -function blockTest_setUp() { - defineTestBlocks(); - workspace = new Blockly.Workspace(); -} - -function blockTest_tearDown() { - undefineTestBlocks(); - workspace.dispose(); - if (mockControl_) { - mockControl_.restore(); - } -} - -function assertUnpluggedNoheal(blocks) { - // A has nothing connected to it. - assertEquals(0, blocks.A.getChildren().length); - // B and C are still connected. - assertEquals(blocks.B, blocks.C.getParent()); - // B is the top of its stack. - assertNull(blocks.B.getParent()); -} - -function assertUnpluggedHealed(blocks) { - // A and C are connected. - assertEquals(1, blocks.A.getChildren().length); - assertEquals(blocks.A, blocks.C.getParent()); - // B has nothing connected to it. - assertEquals(0, blocks.B.getChildren().length); - // B is the top of its stack. - assertNull(blocks.B.getParent()); -} - -function assertUnpluggedHealFailed(blocks) { - // A has nothing connected to it. - assertEquals(0, blocks.A.getChildren().length); - // B has nothing connected to it. - assertEquals(0, blocks.B.getChildren().length); - // B is the top of its stack. - assertNull(blocks.B.getParent()); - // C is the top of its stack. - assertNull(blocks.C.getParent()); -} - -function setUpRowBlocks() { - var blockA = workspace.newBlock('row_block'); - var blockB = workspace.newBlock('row_block'); - var blockC = workspace.newBlock('row_block'); - - blockA.inputList[0].connection.connect(blockB.outputConnection); - blockB.inputList[0].connection.connect(blockC.outputConnection); - - assertEquals(blockB, blockC.getParent()); - - return { - A: blockA, - B: blockB, - C: blockC - }; -} - -function setUpStackBlocks() { - var blockA = workspace.newBlock('stack_block'); - var blockB = workspace.newBlock('stack_block'); - var blockC = workspace.newBlock('stack_block'); - - blockA.nextConnection.connect(blockB.previousConnection); - blockB.nextConnection.connect(blockC.previousConnection); - - assertEquals(blockB, blockC.getParent()); - - return { - A: blockA, - B: blockB, - C: blockC - }; -} - - -function test_block_stack_unplug_noheal() { - blockTest_setUp(); - try { - var blocks = setUpStackBlocks(); - blocks.B.unplug(); - assertUnpluggedNoheal(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_stack_unplug_heal() { - blockTest_setUp(); - try { - var blocks = setUpStackBlocks(); - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_stack_unplug_heal_bad_checks() { - blockTest_setUp(); - try { - var blocks = setUpStackBlocks(); - - // A and C can't connect, but both can connect to B. - blocks.A.nextConnection.setCheck('type1'); - blocks.C.previousConnection.setCheck('type2'); - - // The types don't work. - blocks.B.unplug(true); - - assertUnpluggedHealFailed(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_row_unplug_noheal() { - blockTest_setUp(); - try { - var blocks = setUpRowBlocks(); - blocks.B.unplug(false); - assertUnpluggedNoheal(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_row_unplug_heal() { - blockTest_setUp(); - try { - var blocks = setUpRowBlocks(); - // Each block has only one input, and the types work. - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_row_unplug_heal_bad_checks() { - blockTest_setUp(); - try { - var blocks = setUpRowBlocks(); - - // A and C can't connect, but both can connect to B. - blocks.A.inputList[0].connection.setCheck('type1'); - blocks.C.outputConnection.setCheck('type2'); - - // Each block has only one input, but the types don't work. - blocks.B.unplug(true); - assertUnpluggedHealFailed(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_row_unplug_multi_inputs_parent() { - blockTest_setUp(); - try { - var blocks = setUpRowBlocks(); - // Add extra input to parent - blocks.A.appendValueInput('INPUT').setCheck(null); - - // Parent block has multiple inputs. - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_row_unplug_multi_inputs_middle() { - blockTest_setUp(); - try { - var blocks = setUpRowBlocks(); - // Add extra input to middle block - blocks.B.appendValueInput('INPUT').setCheck(null); - - // Middle block has multiple inputs. - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - } finally { - blockTest_tearDown(); - } -} - -function test_block_row_unplug_multi_inputs_child() { - blockTest_setUp(); - try { - var blocks = setUpRowBlocks(); - // Add extra input to child block - blocks.C.appendValueInput('INPUT').setCheck(null); - - // Child block input count doesn't matter. - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - } finally { - blockTest_tearDown(); - } -} diff --git a/tests/jsunit/event_test.js b/tests/jsunit/event_test.js deleted file mode 100644 index 1e2414a91..000000000 --- a/tests/jsunit/event_test.js +++ /dev/null @@ -1,798 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - - /** - * @fileoverview Tests for Blockly.Events - * @author marisaleung@google.com (Marisa Leung) - */ -'use strict'; - -var mockControl_; -var workspace; - -function eventTest_setUp() { - workspace = new Blockly.Workspace(); -} - -function eventTest_setUpWithMockBlocks() { - eventTest_setUp(); - // TODO: Replace with defineGetVarBlock(); - Blockly.defineBlocksWithJsonArray([{ - 'type': 'field_variable_test_block', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_variable', - 'name': 'VAR', - 'variable': 'item' - } - ], - }, - { - 'type': 'simple_test_block', - 'message0': 'simple test block' - }]); -} - -function eventTest_tearDown() { - delete Blockly.Blocks['field_variable_test_block']; - delete Blockly.Blocks['simple_test_block']; - if (mockControl_) { - mockControl_.restore(); - } - workspace.dispose(); -} - -function eventTest_tearDownWithMockBlocks() { - eventTest_tearDown(); - delete Blockly.Blocks.field_variable_test_block; -} - -function test_block_base_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, '1'); - try { - var block = createSimpleTestBlock(workspace); - - // Here's the event we care about. - var event = new Blockly.Events.BlockBase(block); - assertUndefined(event.varId); - checkExactEventValues(event, {'blockId': '1', 'workspaceId': workspace.id, - 'group': '', 'recordUndo': true}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_var_base_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, '1'); - try { - var variable = workspace.createVariable('name1', 'type1', 'id1'); - - var event = new Blockly.Events.VarBase(variable); - assertUndefined(event.blockId); - checkExactEventValues(event, {'varId': 'id1', - 'workspaceId': workspace.id, 'group': '', 'recordUndo': true}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_abstract_constructor() { - eventTest_setUpWithMockBlocks(); - try { - var event = new Blockly.Events.Abstract(); - assertUndefined(event.blockId); - assertUndefined(event.workspaceId); - assertUndefined(event.varId); - checkExactEventValues(event, {'group': '', 'recordUndo': true}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -// Test util -function checkCreateEventValues(event, block, ids, type) { - var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block)); - var result_xml = Blockly.Xml.domToText(event.xml); - assertEquals(expected_xml, result_xml); - isEqualArrays(ids, event.ids); - assertEquals(type, event.type); -} - -// Test util -function checkDeleteEventValues(event, block, ids, type) { - var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block)); - var result_xml = Blockly.Xml.domToText(event.oldXml); - assertEquals(expected_xml, result_xml); - isEqualArrays(ids, event.ids); - assertEquals(type, event.type); -} - -// Test util -function checkExactEventValues(event, values) { - var keys = Object.keys(values); - for (var i = 0, field; field = keys[i]; i++) { - assertEquals(values[field], event[field]); - } -} - -// Test util -function createSimpleTestBlock(workspace) { - // Disable events while constructing the block: this is a test of the - // Blockly.Event constructors, not the block constructor. - Blockly.Events.disable(); - var block = new Blockly.Block(workspace, 'simple_test_block'); - Blockly.Events.enable(); - return block; -} - -function test_create_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - var block = createSimpleTestBlock(workspace); - - var event = new Blockly.Events.Create(block); - checkCreateEventValues(event, block, ['1'], 'create'); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_blockCreate_constructor() { - // expect that blockCreate behaves the same as create. - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - var block = createSimpleTestBlock(workspace); - - var event = new Blockly.Events.BlockCreate(block); - checkCreateEventValues(event, block, ['1'], 'create'); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_delete_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - var block = createSimpleTestBlock(workspace); - var event = new Blockly.Events.Delete(block); - checkDeleteEventValues(event, block, ['1'], 'delete'); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_blockDelete_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - var block = createSimpleTestBlock(workspace); - block.setCommentText('test comment'); - var event = new Blockly.Events.BlockDelete(block); - checkDeleteEventValues(event, block, ['1'], 'delete'); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_change_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - Blockly.Events.disable(); - var block = new Blockly.Block(workspace, 'field_variable_test_block'); - Blockly.Events.enable(); - - var event = new Blockly.Events.Change(block, 'field', 'VAR', 'id1', 'id2'); - checkExactEventValues(event, {'element': 'field', 'name': 'VAR', - 'oldValue': 'id1', 'newValue': 'id2', 'type': 'change'}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_blockChange_constructor() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - Blockly.Events.disable(); - var block = new Blockly.Block(workspace, 'field_variable_test_block'); - Blockly.Events.enable(); - - var event = new Blockly.Events.BlockChange(block, 'field', 'VAR', 'id1', - 'id2'); - checkExactEventValues(event, {'element': 'field', 'name': 'VAR', - 'oldValue': 'id1', 'newValue': 'id2', 'type': 'change'}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_move_constructorCoordinate() { - // Expect the oldCoordinate to be set. - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2']); - try { - var block1 = createSimpleTestBlock(workspace); - var coordinate = new Blockly.utils.Coordinate(3, 4); - block1.xy_ = coordinate; - - var event = new Blockly.Events.Move(block1); - checkExactEventValues(event, {'oldCoordinate': coordinate, - 'type': 'move'}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_move_constructoroldParentId() { - // Expect the oldParentId to be set but not the oldCoordinate to be set. - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2']); - try { - var block1 = createSimpleTestBlock(workspace); - var block2 = createSimpleTestBlock(workspace); - block1.parentBlock_ = block2; - block1.xy_ = new Blockly.utils.Coordinate(3, 4); - - var event = new Blockly.Events.Move(block1); - checkExactEventValues(event, {'oldCoordinate': undefined, - 'oldParentId': '2', 'type': 'move'}); - block1.parentBlock_ = null; - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_blockMove_constructorCoordinate() { - // Expect the oldCoordinate to be set. - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2']); - try { - var block1 = createSimpleTestBlock(workspace); - var coordinate = new Blockly.utils.Coordinate(3, 4); - block1.xy_ = coordinate; - - var event = new Blockly.Events.BlockMove(block1); - checkExactEventValues(event, {'oldCoordinate': coordinate, - 'type': 'move'}); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_blockMove_constructoroldParentId() { - // Expect the oldParentId to be set but not the oldCoordinate to be set. - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2']); - try { - var block1 = createSimpleTestBlock(workspace); - var block2 = createSimpleTestBlock(workspace); - block1.parentBlock_ = block2; - block1.xy_ = new Blockly.utils.Coordinate(3, 4); - - var event = new Blockly.Events.BlockMove(block1); - checkExactEventValues(event, {'oldCoordinate': undefined, - 'oldParentId': '2', 'type': 'move'}); - block1.parentBlock_ = null; - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_uiEvent_constructor_null() { - try { - Blockly.Events.setGroup('testGroup'); - var event = new Blockly.Events.Ui(null, 'foo', 'bar', 'baz'); - checkExactEventValues(event, - { - 'blockId': null, - 'workspaceId': null, - 'type': 'ui', - 'oldValue': 'bar', - 'newValue': 'baz', - 'element': 'foo', - 'recordUndo': false, - 'group': 'testGroup' - } - ); - } finally { - Blockly.Events.setGroup(false); - } -} - -function test_uiEvent_constructor_block() { - eventTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - try { - var block1 = createSimpleTestBlock(workspace); - Blockly.Events.setGroup('testGroup'); - var event = new Blockly.Events.Ui(block1, 'foo', 'bar', 'baz'); - checkExactEventValues(event, - { - 'blockId': '1', - 'workspaceId': workspace.id, - 'type': 'ui', - 'oldValue': 'bar', - 'newValue': 'baz', - 'element': 'foo', - 'recordUndo': false, - 'group': 'testGroup' - } - ); - } finally { - Blockly.Events.setGroup(false); - eventTest_tearDownWithMockBlocks(); - } -} - -function test_varCreate_constructor() { - eventTest_setUp(); - try { - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarCreate(variable); - checkExactEventValues(event, {'varName': 'name1', 'varType': 'type1', - 'type': 'var_create'}); - } finally { - eventTest_tearDown(); - } -} - -function test_varCreate_toJson() { - eventTest_setUp(); - try { - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarCreate(variable); - var json = event.toJson(); - var expectedJson = ({type: "var_create", varId: "id1", varType: "type1", - varName: "name1"}); - - assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); - } finally { - eventTest_tearDown(); - } -} - -function test_varCreate_fromJson() { - eventTest_setUp(); - try { - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarCreate(variable); - var event2 = new Blockly.Events.VarCreate(null); - var json = event.toJson(); - event2.fromJson(json); - - assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); - } finally { - eventTest_tearDown(); - } -} - -function test_varCreate_runForward() { - eventTest_setUp(); - var json = {type: "var_create", varId: "id1", varType: "type1", - varName: "name1"}; - var event = Blockly.Events.fromJson(json, workspace); - assertNull(workspace.getVariableById('id1')); - event.run(true); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - eventTest_tearDown(); -} - -function test_varCreate_runBackwards() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarCreate(variable); - assertNotNull(workspace.getVariableById('id1')); - event.run(false); - assertNull(workspace.getVariableById('id1')); - eventTest_tearDown(); -} - -function test_varDelete_constructor() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarDelete(variable); - checkExactEventValues(event, {'varName': 'name1', 'varType': 'type1', - 'varId':'id1', 'type': 'var_delete'}); - eventTest_tearDown(); -} - -function test_varDelete_toJson() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarDelete(variable); - var json = event.toJson(); - var expectedJson = ({type: "var_delete", varId: "id1", varType: "type1", - varName: "name1"}); - - assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); - eventTest_tearDown(); -} - -function test_varDelete_fromJson() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarDelete(variable); - var event2 = new Blockly.Events.VarDelete(null); - var json = event.toJson(); - event2.fromJson(json); - - assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); - eventTest_tearDown(); -} - -function test_varDelete_runForwards() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarDelete(variable); - assertNotNull(workspace.getVariableById('id1')); - event.run(true); - assertNull(workspace.getVariableById('id1')); - eventTest_tearDown(); -} - -function test_varDelete_runBackwards() { - eventTest_setUp(); - var json = {type: "var_delete", varId: "id1", varType: "type1", - varName: "name1"}; - var event = Blockly.Events.fromJson(json, workspace); - assertNull(workspace.getVariableById('id1')); - event.run(false); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - eventTest_tearDown(); -} - -function test_varRename_constructor() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarRename(variable, 'name2'); - checkExactEventValues(event, {'varId': 'id1', 'oldName': 'name1', - 'newName': 'name2', 'type': 'var_rename'}); - eventTest_tearDown(); -} - -function test_varRename_toJson() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarRename(variable, 'name2'); - var json = event.toJson(); - var expectedJson = ({type: "var_rename", varId: "id1", oldName: "name1", - newName: "name2"}); - - assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); - eventTest_tearDown(); -} - -function test_varRename_fromJson() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarRename(variable, ''); - var event2 = new Blockly.Events.VarRename(null); - var json = event.toJson(); - event2.fromJson(json); - - assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); - eventTest_tearDown(); -} - -function test_varRename_runForward() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarRename(variable, 'name2'); - event.run(true); - assertNull(workspace.getVariable('name1')); - checkVariableValues(workspace, 'name2', 'type1', 'id1'); - eventTest_tearDown(); -} - -function test_varBackard_runForward() { - eventTest_setUp(); - var variable = workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarRename(variable, 'name2'); - event.run(false); - assertNull(workspace.getVariable('name2')); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - eventTest_tearDown(); -} - -function test_events_filter() { - eventTest_setUpWithMockBlocks(); - try { - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var events = [ - new Blockly.Events.BlockCreate(block1), - new Blockly.Events.BlockMove(block1), - new Blockly.Events.BlockChange(block1, 'field', 'VAR', 'id1', 'id2'), - new Blockly.Events.Ui(block1, 'click') - ]; - var filteredEvents = Blockly.Events.filter(events, true); - assertEquals(4, filteredEvents.length); // no event should have been removed. - // test that the order hasn't changed - assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); - assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); - assertTrue(filteredEvents[2] instanceof Blockly.Events.BlockChange); - assertTrue(filteredEvents[3] instanceof Blockly.Events.Ui); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_events_filterForward() { - eventTest_setUpWithMockBlocks(); - try { - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var events = [ - new Blockly.Events.BlockCreate(block1), - ]; - helper_addMoveEvent(events, block1, 1, 1); - helper_addMoveEvent(events, block1, 2, 2); - helper_addMoveEvent(events, block1, 3, 3); - var filteredEvents = Blockly.Events.filter(events, true); - assertEquals(2, filteredEvents.length); // duplicate moves should have been removed. - // test that the order hasn't changed - assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); - assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); - assertEquals(3, filteredEvents[1].newCoordinate.x); - assertEquals(3, filteredEvents[1].newCoordinate.y); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_events_filterBackward() { - eventTest_setUpWithMockBlocks(); - try { - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var events = [ - new Blockly.Events.BlockCreate(block1), - ]; - helper_addMoveEvent(events, block1, 1, 1); - helper_addMoveEvent(events, block1, 2, 2); - helper_addMoveEvent(events, block1, 3, 3); - var filteredEvents = Blockly.Events.filter(events, false); - assertEquals(2, filteredEvents.length); // duplicate event should have been removed. - // test that the order hasn't changed - assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); - assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); - assertEquals(1, filteredEvents[1].newCoordinate.x); - assertEquals(1, filteredEvents[1].newCoordinate.y); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} - -function test_events_filterDifferentBlocks() { - eventTest_setUpWithMockBlocks(); - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var block2 = workspace.newBlock('field_variable_test_block', '2'); - var events = [ - new Blockly.Events.BlockCreate(block1), - new Blockly.Events.BlockMove(block1), - new Blockly.Events.BlockCreate(block2), - new Blockly.Events.BlockMove(block2) - ]; - var filteredEvents = Blockly.Events.filter(events, true); - assertEquals(4, filteredEvents.length); // no event should have been removed. - eventTest_tearDownWithMockBlocks(); -} - -function test_events_mergeMove() { - eventTest_setUpWithMockBlocks(); - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var events = []; - helper_addMoveEvent(events, block1, 0, 0); - helper_addMoveEvent(events, block1, 1, 1); - var filteredEvents = Blockly.Events.filter(events, true); - assertEquals(1, filteredEvents.length); // second move event merged into first - assertEquals(1, filteredEvents[0].newCoordinate.x); - assertEquals(1, filteredEvents[0].newCoordinate.y); - eventTest_tearDownWithMockBlocks(); -} - -function test_events_mergeChange() { - eventTest_setUpWithMockBlocks(); - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var events = [ - new Blockly.Events.Change(block1, 'field', 'VAR', 'item', 'item1'), - new Blockly.Events.Change(block1, 'field', 'VAR', 'item1', 'item2') - ]; - var filteredEvents = Blockly.Events.filter(events, true); - assertEquals(1, filteredEvents.length); // second change event merged into first - assertEquals('item', filteredEvents[0].oldValue); - assertEquals('item2', filteredEvents[0].newValue); - eventTest_tearDownWithMockBlocks(); -} - -function test_events_mergeUi() { - eventTest_setUpWithMockBlocks(); - var block1 = workspace.newBlock('field_variable_test_block', '1'); - var block2 = workspace.newBlock('field_variable_test_block', '2'); - var block3 = workspace.newBlock('field_variable_test_block', '3'); - var events = [ - new Blockly.Events.Ui(block1, 'commentOpen', 'false', 'true'), - new Blockly.Events.Ui(block1, 'click', 'false', 'true'), - new Blockly.Events.Ui(block2, 'mutatorOpen', 'false', 'true'), - new Blockly.Events.Ui(block2, 'click', 'false', 'true'), - new Blockly.Events.Ui(block3, 'warningOpen', 'false', 'true'), - new Blockly.Events.Ui(block3, 'click', 'false', 'true') - ]; - var filteredEvents = Blockly.Events.filter(events, true); - assertEquals(3, filteredEvents.length); // click event merged into corresponding *Open event - assertEquals('commentOpen', filteredEvents[0].element); - assertEquals('mutatorOpen', filteredEvents[1].element); - assertEquals('warningOpen', filteredEvents[2].element); - eventTest_tearDownWithMockBlocks(); -} - -/** - * Tests that events that collide on a (event, block, workspace) tuple - * but cannot be merged do not get dropped during filtering. - */ -function test_events_stackclick() { - eventTest_setUpWithMockBlocks(); - var block = workspace.newBlock('field_variable_test_block', '1'); - var events = [ - new Blockly.Events.Ui(block, 'click', undefined, undefined), - new Blockly.Events.Ui(block, 'stackclick', undefined, undefined) - ]; - var filteredEvents = Blockly.Events.filter(events, true); - // click and stackclick should both exist - assertEquals(2, filteredEvents.length); - assertEquals('click', filteredEvents[0].element); - assertEquals('stackclick', filteredEvents[1].element); - eventTest_tearDownWithMockBlocks(); -} - -/** - * Mutator composition could result in move events for blocks - * connected to the mutated block that were null operations. This - * leads to events in the undo/redo queue that do nothing, requiring - * an extra undo/redo to proceed to the next event. This test ensures - * that two move events that do get merged (disconnecting and - * reconnecting a block in response to a mutator change) are filtered - * from the queue. - */ -function test_events_filteraftermerge() { - eventTest_setUpWithMockBlocks(); - var block = workspace.newBlock('field_variable_test_block', '1'); - block.setParent(null); - var events = []; - helper_addMoveEventParent(events, block, null); - helper_addMoveEventParent(events, block, null); - var filteredEvents = Blockly.Events.filter(events, true); - // The two events should be merged, but because nothing has changed - // they will be filtered out. - assertEquals(0, filteredEvents.length); - eventTest_tearDownWithMockBlocks(); -} - -/** - * Helper function to simulate block move events. - * - * @param {!Array.} events a queue of events. - * @param {!Blockly.Block} block the block to be moved - * @param {number} newX new X coordinate of the block - * @param {number} newY new Y coordinate of the block - */ -function helper_addMoveEvent(events, block, newX, newY) { - events.push(new Blockly.Events.BlockMove(block)); - block.xy_ = new Blockly.utils.Coordinate(newX, newY); - events[events.length-1].recordNew(); -} - -function helper_addMoveEventParent(events, block, parent) { - events.push(new Blockly.Events.BlockMove(block)); - block.setParent(parent); - events[events.length-1].recordNew(); -} - -function test_events_newblock_newvar() { - eventTest_setUpWithMockBlocks(); - - Blockly.Events.fire = temporary_fireEvent; - temporary_fireEvent.firedEvents_ = []; - // Expect three calls to genUid: one to set the block's ID, one for the event - // group's id, and one for the variable's ID. - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2', '3']); - try { - var block = workspace.newBlock('field_variable_test_block'); - - var firedEvents = workspace.undoStack_; - // Expect two events: varCreate and block create. - assertEquals(2, firedEvents.length); - - var event0 = firedEvents[0]; - var event1 = firedEvents[1]; - assertEquals('var_create', event0.type); - assertEquals('create', event1.type); - - // Expect the events to have the same group ID. - assertEquals(event0.group, event1.group); - - // Expect the group ID to be the result of the second call to genUid. - assertEquals('2', event0.group); - - // Expect the workspace to have a variable with ID '3'. - assertNotNull(workspace.getVariableById('3')); - assertEquals('3', event0.varId); - } finally { - eventTest_tearDownWithMockBlocks(); - Blockly.Events.fire = savedFireFunc; - } -} - -// The sequence of events should be the same whether the block was created from -// XML or directly. -function test_events_newblock_newvar_xml() { - eventTest_setUpWithMockBlocks(); - - Blockly.Events.fire = temporary_fireEvent; - temporary_fireEvent.firedEvents_ = []; - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' name1' + - ' ' + - ''); - Blockly.Xml.domToWorkspace(dom, workspace); - - var firedEvents = workspace.undoStack_; - // Expect two events: varCreate and block create. - assertEquals(2, firedEvents.length); - - var event0 = firedEvents[0]; - var event1 = firedEvents[1]; - assertEquals('var_create', event0.type); - assertEquals('create', event1.type); - - // Expect the events to have the same group ID. - assertEquals(event0.group, event1.group); - - // Expect the workspace to have a variable with ID 'id1'. - assertNotNull(workspace.getVariableById('id1')); - assertEquals('id1', event0.varId); - } finally { - eventTest_tearDownWithMockBlocks(); - Blockly.Events.fire = savedFireFunc; - } -} - -function test_events_filter_nomerge_move() { - // Move events should only merge if they refer to the same block and are - // consecutive. - // See github.com/google/blockly/pull/1892 for a worked example showing - // how merging non-consecutive events can fail when replacing a shadow - // block. - eventTest_setUpWithMockBlocks(); - try { - var block1 = createSimpleTestBlock(workspace); - var block2 = createSimpleTestBlock(workspace); - - var events = []; - helper_addMoveEvent(events, block1, 1, 1); - helper_addMoveEvent(events, block2, 1, 1); - events.push(new Blockly.Events.BlockDelete(block2)); - helper_addMoveEvent(events, block1, 2, 2); - - var filteredEvents = Blockly.Events.filter(events, true); - // Nothing should have merged. - assertEquals(4, filteredEvents.length); - // test that the order hasn't changed - assertTrue(filteredEvents[0] instanceof Blockly.Events.BlockMove); - assertTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); - assertTrue(filteredEvents[2] instanceof Blockly.Events.BlockDelete); - assertTrue(filteredEvents[3] instanceof Blockly.Events.BlockMove); - } finally { - eventTest_tearDownWithMockBlocks(); - } -} diff --git a/tests/jsunit/extensions_test.js b/tests/jsunit/extensions_test.js deleted file mode 100644 index 43bb66deb..000000000 --- a/tests/jsunit/extensions_test.js +++ /dev/null @@ -1,643 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - - /** - * @fileoverview Tests for Blockly.Extensions - * @author Anm@anm.me (Andrew n marshall) - */ -'use strict'; - -function test_extension() { - var workspace = new Blockly.Workspace(); - var block; - try { - assertUndefined(Blockly.Extensions.ALL_['extensions_test']); - - var numCallsToBefore = 0; - var numCallsToAfter = 0; - - // Extension defined before the block type is defined. - Blockly.Extensions.register('extensions_test_before', function () { - numCallsToBefore++; - this.extendedWithBefore = true; - }); - - Blockly.defineBlocksWithJsonArray([{ - "type": "extension_test_block", - "message0": "extension_test_block", - "extensions": ["extensions_test_before", "extensions_test_after"] - }]); - - // Extension defined after the block type (but before instantiation). - Blockly.Extensions.register('extensions_test_after', function () { - numCallsToAfter++; - this.extendedWithAfter = true; - }); - - assert(typeof Blockly.Extensions.ALL_['extensions_test_before'] == 'function'); - assert(typeof Blockly.Extensions.ALL_['extensions_test_after'] == 'function'); - assertEquals(0, numCallsToBefore); - assertEquals(0, numCallsToAfter); - - block = new Blockly.Block(workspace, 'extension_test_block'); - - assertEquals(1, numCallsToBefore); - assertEquals(1, numCallsToAfter); - assert(block.extendedWithBefore); - assert(block.extendedWithAfter); - } finally { - block && block.dispose(); - workspace.dispose(); - - delete Blockly.Extensions.ALL_['extensions_test_before']; - delete Blockly.Extensions.ALL_['extensions_test_after']; - delete Blockly.Blocks['extension_test_block']; - } -} - -function test_extension_missing() { - var workspace = new Blockly.Workspace(); - var block; - var exceptionWasThrown = false; - try { - assertUndefined(Blockly.Extensions.ALL_['missing_extension']); - Blockly.defineBlocksWithJsonArray([{ - "type": "missing_extension_block", - "message0": "missing_extension_block", - "extensions": ["missing_extension"] - }]); - - block = new Blockly.Block(workspace, 'missing_extension_block'); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - block && block.dispose(); - workspace.dispose(); - delete Blockly.Blocks['missing_extension_block']; - } - assert(exceptionWasThrown); -} - -function test_extension_not_a_function() { - var exceptionWasThrown = false; - try { - assertUndefined(Blockly.Extensions.ALL_['extension_just_a_string']); - Blockly.Extensions.register('extension_just_a_string', 'extension_just_a_string'); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['extension_just_a_string']; - } - assert(exceptionWasThrown); - - var exceptionWasThrown = false; - try { - assertUndefined(Blockly.Extensions.ALL_['extension_is_null']); - Blockly.Extensions.register('extension_is_null', null); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['extension_is_null']; - } - assert(exceptionWasThrown); - - var exceptionWasThrown = false; - try { - assertUndefined(Blockly.Extensions.ALL_['extension_is_undefined']); - Blockly.Extensions.register('extension_is_undefined'); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['extension_is_undefined']; - } - assert(exceptionWasThrown); -} - -function test_parent_tooltip_when_inline() { - var defaultTooltip = "defaultTooltip"; - var parentTooltip = "parentTooltip"; - - var workspace = new Blockly.Workspace(); - var block; - try { - Blockly.defineBlocksWithJsonArray([ - { - "type": "test_parent_tooltip_when_inline", - "message0": "test_parent_tooltip_when_inline", - "output": true, - "tooltip": defaultTooltip, - "extensions": ["parent_tooltip_when_inline"] - }, - { - "type": "test_parent", - "message0": "%1", - "args0": [ - { - "type": "input_value", - "name": "INPUT" - } - ], - "tooltip": parentTooltip - } - ]); - - block = new Blockly.Block(workspace, 'test_parent_tooltip_when_inline'); - - // Tooltip is dynamic after extension initialization. - assert(typeof block.tooltip == 'function'); - assertEquals(block.tooltip(), defaultTooltip); - - // Tooltip is normal before connected to parent. - var parent = new Blockly.Block(workspace, 'test_parent'); - assertEquals(parent.tooltip, parentTooltip); - assertFalse(!!parent.inputsInline); - - // Tooltip is normal when parent is not inline. - parent.getInput('INPUT').connection.connect(block.outputConnection); - assertEquals(block.getParent(), parent); - assertEquals(block.tooltip(), defaultTooltip); - - // Tooltip is parent's when parent is inline. - parent.setInputsInline(true); - assertEquals(block.tooltip(), parentTooltip); - - // Tooltip revert when disconnected. - parent.getInput('INPUT').connection.disconnect(); - assert(!block.getParent()); - assertEquals(block.tooltip(), defaultTooltip); - } finally { - block && block.dispose(); - workspace.dispose(); - - delete Blockly.Blocks['test_parent_tooltip_when_inline']; - delete Blockly.Blocks['test_parent']; - } -} - -function test_mixin_extension() { - var TEST_MIXIN = { - field: 'FIELD', - method: function() { - console.log('TEXT_MIXIN method()'); - } - }; - - var workspace = new Blockly.Workspace(); - var block; - try { - assertUndefined(Blockly.Extensions.ALL_['mixin_test']); - - // Extension defined before the block type is defined. - Blockly.Extensions.registerMixin('mixin_test', TEST_MIXIN); - assert(typeof Blockly.Extensions.ALL_['mixin_test'] == 'function'); - - Blockly.defineBlocksWithJsonArray([{ - "type": "test_block_mixin", - "message0": "test_block_mixin", - "extensions": ["mixin_test"] - }]); - - block = new Blockly.Block(workspace, 'test_block_mixin'); - - assertEquals(TEST_MIXIN.field, block.field); - assertEquals(TEST_MIXIN.method, block.method); - } finally { - block && block.dispose(); - workspace.dispose(); - - delete Blockly.Extensions.ALL_['mixin_test']; - delete Blockly.Blocks['test_block_mixin']; - } -} - -function test_bad_mixin_overwrites_local_value() { - var TEST_MIXIN_BAD_INPUTLIST = { - inputList: 'bad inputList' // Defined in constructor - }; - - var workspace = new Blockly.Workspace(); - var block; - try { - assertUndefined(Blockly.Extensions.ALL_['mixin_bad_inputList']); - - // Extension defined before the block type is defined. - Blockly.Extensions.registerMixin('mixin_bad_inputList', TEST_MIXIN_BAD_INPUTLIST); - assert(typeof Blockly.Extensions.ALL_['mixin_bad_inputList'] == 'function'); - - Blockly.defineBlocksWithJsonArray([{ - "type": "test_block_bad_inputList", - "message0": "test_block_bad_inputList", - "extensions": ["mixin_bad_inputList"] - }]); - - try { - block = new Blockly.Block(workspace, 'test_block_bad_inputList'); - } catch (e) { - // Expected Error - assert(e.message.indexOf('inputList') >= 0); // Reference the conflict - return; - } - fail('Expected error when constructing block'); - } finally { - block && block.dispose(); - workspace.dispose(); - - delete Blockly.Extensions.ALL_['mixin_bad_inputList']; - delete Blockly.Blocks['test_block_bad_inputList']; - } -} - -function test_bad_mixin_overwrites_prototype() { - var TEST_MIXIN_BAD_COLOUR = { - colour_: 'bad colour_' // Defined on prototype - }; - - var workspace = new Blockly.Workspace(); - var block; - try { - assertUndefined(Blockly.Extensions.ALL_['mixin_bad_colour_']); - - // Extension defined before the block type is defined. - Blockly.Extensions.registerMixin('mixin_bad_colour_', TEST_MIXIN_BAD_COLOUR); - assert(typeof Blockly.Extensions.ALL_['mixin_bad_colour_'] == 'function'); - - Blockly.defineBlocksWithJsonArray([{ - "type": "test_block_bad_colour", - "message0": "test_block_bad_colour", - "extensions": ["mixin_bad_colour_"] - }]); - - try { - block = new Blockly.Block(workspace, 'test_block_bad_colour'); - } catch (e) { - // Expected Error - assert(e.message.indexOf('colour_') >= 0); // Reference the conflict - return; - } - fail('Expected error when constructing block'); - } finally { - block && block.dispose(); - workspace.dispose(); - - delete Blockly.Extensions.ALL_['mixin_bad_colour_']; - delete Blockly.Blocks['test_block_bad_colour']; - } -} - -function test_mutator_mixin() { - var workspace = new Blockly.Workspace(); - var block; - - try { - Blockly.defineBlocksWithJsonArray([{ - "type": "mutator_test_block", - "message0": "mutator_test_block", - "mutator": "mutator_test" - }]); - - // Events code calls mutationToDom and expects it to give back a meaningful - // value. - Blockly.Events.disable(); - Blockly.Extensions.registerMutator('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - }, - compose: function() { - return 'composeFn'; - }, - decompose: function() { - return 'decomposeFn'; - } - }); - - block = new Blockly.Block(workspace, 'mutator_test_block'); - - // Make sure all of the functions were installed correctly. - assertEquals(block.domToMutation(), 'domToMutationFn'); - assertEquals(block.mutationToDom(), 'mutationToDomFn'); - assertEquals(block.compose(), 'composeFn'); - assertEquals(block.decompose(), 'decomposeFn'); - } finally { - if (block) { - block.dispose(); - } - workspace.dispose(); - Blockly.Events.enable(); - delete Blockly.Extensions.ALL_['mutator_test']; - } -} - -function test_mutator_mixin_no_dialog() { - var workspace = new Blockly.Workspace(); - var block; - - try { - Blockly.defineBlocksWithJsonArray([{ - "type": "mutator_test_block", - "message0": "mutator_test_block", - "mutator": "mutator_test" - }]); - - // Events code calls mutationToDom and expects it to give back a meaningful - // value. - Blockly.Events.disable(); - assertUndefined(Blockly.Extensions.ALL_['mutator_test']); - Blockly.Extensions.registerMutator('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - } - }); - - block = new Blockly.Block(workspace, 'mutator_test_block'); - - // Make sure all of the functions were installed correctly. - assertEquals(block.domToMutation(), 'domToMutationFn'); - assertEquals(block.mutationToDom(), 'mutationToDomFn'); - assertFalse(block.hasOwnProperty('compose')); - assertFalse(block.hasOwnProperty('decompose')); - } finally { - if (block) { - block.dispose(); - } - workspace.dispose(); - Blockly.Events.enable(); - delete Blockly.Extensions.ALL_['mutator_test']; - } -} - -// Explicitly check all four things that could be missing. -function test_mutator_mixin_no_decompose_fails() { - var exceptionWasThrown = false; - try { - Blockly.Extensions.registerMutator('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - }, - compose: function() { - return 'composeFn'; - } - }); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['mutator_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_mutator_mixin_no_compose_fails() { - var exceptionWasThrown = false; - try { - Blockly.Extensions.registerMutator('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - }, - decompose: function() { - return 'decomposeFn'; - } - }); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['mutator_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_mutator_mixin_no_domToMutation_fails() { - var exceptionWasThrown = false; - try { - Blockly.Extensions.registerMutator('mutator_test', - { - mutationToDom: function() { - return 'mutationToDomFn'; - }, - compose: function() { - return 'composeFn'; - }, - decompose: function() { - return 'decomposeFn'; - } - }); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['mutator_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_mutator_mixin_no_mutationToDom_fails() { - var exceptionWasThrown = false; - try { - Blockly.Extensions.registerMutator('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - compose: function() { - return 'composeFn'; - }, - decompose: function() { - return 'decomposeFn'; - } - }); - } catch (e) { - // Expected. - exceptionWasThrown = true; - } finally { - delete Blockly.Extensions.ALL_['mutator_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_use_mutator_as_extension_fails() { - var workspace = new Blockly.Workspace(); - var block; - var exceptionWasThrown = false; - - try { - Blockly.defineBlocksWithJsonArray([{ - "type": "mutator_test_block", - "message0": "mutator_test_block", - "extensions": ["mutator_test"] - }]); - - Blockly.Events.disable(); - assertUndefined(Blockly.Extensions.ALL_['mutator_test']); - Blockly.Extensions.registerMutator('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - } - }); - - // Events code calls mutationToDom and expects it to give back a meaningful - // value. - block = new Blockly.Block(workspace, 'mutator_test_block'); - } catch (e) { - // Expected - exceptionWasThrown = true; - // Should have failed on apply, not on register. - assertNotNull(Blockly.Extensions.ALL_['mutator_test']); - } finally { - if (block) { - block.dispose(); - } - workspace.dispose(); - Blockly.Events.enable(); - delete Blockly.Extensions.ALL_['mutator_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_use_mutator_mixin_as_extension_fails() { - var workspace = new Blockly.Workspace(); - var block; - var exceptionWasThrown = false; - - try { - Blockly.defineBlocksWithJsonArray([{ - "type": "mutator_test_block", - "message0": "mutator_test_block", - "extensions": ["mutator_test"] - }]); - - // Events code calls mutationToDom and expects it to give back a meaningful - // value. - Blockly.Events.disable(); - assertUndefined(Blockly.Extensions.ALL_['mutator_test']); - Blockly.Extensions.registerMixin('mutator_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - } - }); - - block = new Blockly.Block(workspace, 'mutator_test_block'); - } catch (e) { - // Expected - exceptionWasThrown = true; - // Should have failed on apply, not on register. - assertNotNull(Blockly.Extensions.ALL_['mutator_test']); - } finally { - if (block) { - block.dispose(); - } - workspace.dispose(); - Blockly.Events.enable(); - delete Blockly.Extensions.ALL_['mutator_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_use_extension_as_mutator_fails() { - var workspace = new Blockly.Workspace(); - var block; - var exceptionWasThrown = false; - - try { - Blockly.defineBlocksWithJsonArray([{ - "type": "mutator_test_block", - "message0": "mutator_test_block", - "mutator": ["extensions_test"] - }]); - - // Events code calls mutationToDom and expects it to give back a meaningful - // value. - Blockly.Events.disable(); - assertUndefined(Blockly.Extensions.ALL_['extensions_test']); - Blockly.Extensions.register('extensions_test', function() { - return 'extensions_test_fn'; - }); - - block = new Blockly.Block(workspace, 'mutator_test_block'); - } catch (e) { - // Expected - exceptionWasThrown = true; - // Should have failed on apply, not on register. - assertNotNull(Blockly.Extensions.ALL_['extensions_test']); - } finally { - if (block) { - block.dispose(); - } - workspace.dispose(); - Blockly.Events.enable(); - delete Blockly.Extensions.ALL_['extensions_test']; - } - assertTrue(exceptionWasThrown); -} - -function test_mutator_mixin_plus_function() { - var workspace = new Blockly.Workspace(); - var block; - var fnWasCalled = false; - - try { - Blockly.defineBlocksWithJsonArray([{ - "type": "mutator_test_block", - "message0": "mutator_test_block", - "mutator": ["extensions_test"] - }]); - - Blockly.Events.disable(); - assertUndefined(Blockly.Extensions.ALL_['extensions_test']); - Blockly.Extensions.registerMutator('extensions_test', - { - domToMutation: function() { - return 'domToMutationFn'; - }, - mutationToDom: function() { - return 'mutationToDomFn'; - } - }, - function() { - fnWasCalled = true; - } - ); - - // Events code calls mutationToDom and expects it to give back a meaningful - // value. - block = new Blockly.Block(workspace, 'mutator_test_block'); - } finally { - if (block) { - block.dispose(); - } - workspace.dispose(); - Blockly.Events.enable(); - delete Blockly.Extensions.ALL_['extensions_test']; - } - assertTrue(fnWasCalled); -} diff --git a/tests/jsunit/generator_test.js b/tests/jsunit/generator_test.js deleted file mode 100644 index b7187991b..000000000 --- a/tests/jsunit/generator_test.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -goog.require('Blockly.Dart'); -goog.require('Blockly.JavaScript'); -goog.require('Blockly.Lua'); -goog.require('Blockly.PHP'); -goog.require('Blockly.Python'); - -var workspace; - -function defineGeneratorTestBlocks() { - Blockly.defineBlocksWithJsonArray([{ - "type": "stack_block", - "message0": "", - "previousStatement": null, - "nextStatement": null - }, - { - "type": "row_block", - "message0": "%1", - "args0": [ - { - "type": "input_value", - "name": "INPUT" - } - ], - "output": null, - "nextStatement": null - }, - { - "type": "controls_repeat_ext", - "message0": "Repeat Loop", - "message1": "%1", - "args1": [{ - "type": "input_statement", - "name": "DO" - }], - "previousStatement": null, - "nextStatement": null - }]); -} - -function undefineGeneratorTestBlocks() { - delete Blockly.Blocks['stack_block']; - delete Blockly.Blocks['row_block']; -} - -function generatorTest_setUp() { - defineGeneratorTestBlocks(); - workspace = new Blockly.Workspace(); -} - -function generatorTest_tearDown() { - undefineGeneratorTestBlocks(); - workspace.dispose(); -} - -function blockToCodeTestSetup(generator) { - var row_block = workspace.newBlock('row_block'); - var stack_block = workspace.newBlock('stack_block'); - - generator.row_block = function(block){return 'row_block'}; - generator.stack_block = function(block){return 'stack_block'}; - row_block.nextConnection.connect(stack_block.previousConnection); - return row_block; -} - -function blockToCodeTest(generator, opt_thisOnly) { - var row_block = blockToCodeTestSetup(generator); - return generator.blockToCode(row_block, opt_thisOnly); -} - -function disabledBlockTest(generator, opt_thisOnly) { - var row_block = blockToCodeTestSetup(generator); - row_block.disabled=true; - return generator.blockToCode(row_block, opt_thisOnly); -} - -function nestedLoopTest(generator, opt_thisOnly) { - Blockly.Msg['CONTROLS_REPEAT_TITLE'] = 'repeat %1 times'; - var blockA = workspace.newBlock('controls_repeat_ext'); - var blockB = workspace.newBlock('controls_repeat_ext'); - var blockC = workspace.newBlock('controls_repeat_ext'); - generator.controls_repeat_ext = getControlRepeatFunc(generator); - - blockA.getInput('DO').connection.connect(blockB.previousConnection); - blockA.nextConnection.connect(blockC.previousConnection); - return generator.blockToCode(blockA, opt_thisOnly); -} - -function getControlRepeatFunc(generator) { - return function(block){ - return '{' + generator.statementToCode(block, 'DO') + '}'; - }; -} - -function test_blockToCodeOnAllGeneartors() { - generatorTest_setUp(); - - assertEquals(blockToCodeTest(Blockly.Dart, true), 'row_block'); - assertEquals(blockToCodeTest(Blockly.Dart, false), 'row_blockstack_block'); - - assertEquals(blockToCodeTest(Blockly.JavaScript, true), 'row_block'); - assertEquals(blockToCodeTest(Blockly.JavaScript, false), 'row_blockstack_block'); - - assertEquals(blockToCodeTest(Blockly.Lua, true), 'row_block'); - assertEquals(blockToCodeTest(Blockly.Lua, false), 'row_blockstack_block'); - - assertEquals(blockToCodeTest(Blockly.PHP, true), 'row_block'); - assertEquals(blockToCodeTest(Blockly.PHP, false), 'row_blockstack_block'); - - assertEquals(blockToCodeTest(Blockly.Python, true), 'row_block'); - assertEquals(blockToCodeTest(Blockly.Python, false), 'row_blockstack_block'); - - generatorTest_tearDown(); -} - -function test_disabledBlockToCode() { - generatorTest_setUp(); - - assertEquals(disabledBlockTest(Blockly.Dart, true), ''); - assertEquals(disabledBlockTest(Blockly.Dart, false), 'stack_block'); - - assertEquals(disabledBlockTest(Blockly.JavaScript, true), ''); - assertEquals(disabledBlockTest(Blockly.JavaScript, false), 'stack_block'); - - assertEquals(disabledBlockTest(Blockly.Lua, true), ''); - assertEquals(disabledBlockTest(Blockly.Lua, false), 'stack_block'); - - assertEquals(disabledBlockTest(Blockly.PHP, true), ''); - assertEquals(disabledBlockTest(Blockly.PHP, false), 'stack_block'); - - assertEquals(disabledBlockTest(Blockly.Python, true), ''); - assertEquals(disabledBlockTest(Blockly.Python, false), 'stack_block'); - - generatorTest_tearDown(); -} - -// { {}} represents a nested block -//{ {}}{} represents a nested block with a block after it -function test_nestedLoopBlockToCode() { - generatorTest_setUp(); - - assertEquals(nestedLoopTest(Blockly.Dart, true), '{ {}}'); - assertEquals(nestedLoopTest(Blockly.Dart, false), '{ {}}{}'); - - assertEquals(nestedLoopTest(Blockly.JavaScript, true), '{ {}}'); - assertEquals(nestedLoopTest(Blockly.JavaScript, false), '{ {}}{}'); - - assertEquals(nestedLoopTest(Blockly.Lua, true), '{ {}}'); - assertEquals(nestedLoopTest(Blockly.Lua, false), '{ {}}{}'); - - assertEquals(nestedLoopTest(Blockly.PHP, true), '{ {}}'); - assertEquals(nestedLoopTest(Blockly.PHP, false), '{ {}}{}'); - - assertEquals(nestedLoopTest(Blockly.Python, true), '{ {}}'); - assertEquals(nestedLoopTest(Blockly.Python, false), '{ {}}{}'); - - generatorTest_tearDown(); -} - -function test_prefix() { - var generator = new Blockly.Generator('INTERCAL'); - assertEquals('Prefix nothing.', '', generator.prefixLines('', '')); - assertEquals('Prefix a word.', '@Hello', generator.prefixLines('Hello', '@')); - assertEquals('Prefix one line.', '12Hello\n', generator.prefixLines('Hello\n', '12')); - assertEquals('Prefix two lines.', '***Hello\n***World\n', generator.prefixLines('Hello\nWorld\n', '***')); -} \ No newline at end of file diff --git a/tests/jsunit/index.html b/tests/jsunit/index.html deleted file mode 100644 index d7d36a7d6..000000000 --- a/tests/jsunit/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Unit Tests for Blockly - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/jsunit/json_test.js b/tests/jsunit/json_test.js deleted file mode 100644 index 197b8b620..000000000 --- a/tests/jsunit/json_test.js +++ /dev/null @@ -1,356 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -/** Ensure a block can be instantiated from a JSON definition. */ -function test_json_minimal() { - var BLOCK_TYPE = 'test_json_minimal'; - - var workspace = new Blockly.Workspace(); - var block; - try { - var warnings = captureWarnings(function() { - Blockly.defineBlocksWithJsonArray([{ - "type": BLOCK_TYPE - }]); - block = new Blockly.Block(workspace, BLOCK_TYPE); - }); - - assertEquals(BLOCK_TYPE, block.type); - assertEquals( - 'Expecting no warnings when defining and creating a simple block.', - warnings.length, 0); - // TODO: asserts - } finally { - block.dispose(); - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE]; - } -} - -/** - * Ensure a block without a type id fails with a warning, but does not block - * later definitions. - */ -function test_json_nullOrUndefinedBlockTypeId() { - var BLOCK_TYPE1 = 'test_json_before_bad_blocks'; - var BLOCK_TYPE2 = 'test_json_after_bad_blocks'; - - assertUndefined(Blockly.Blocks[BLOCK_TYPE1]); - assertUndefined(Blockly.Blocks[BLOCK_TYPE2]); - var blockTypeCount = Object.keys(Blockly.Blocks).length; - - try { - var warnings = captureWarnings(function() { - Blockly.defineBlocksWithJsonArray([ - {"type": BLOCK_TYPE1}, - {"type": undefined}, - {"type": null}, - {"type": BLOCK_TYPE2}]); - }); - - assertNotNullNorUndefined('Block before bad blocks should be defined.', - Blockly.Blocks[BLOCK_TYPE1]); - assertNotNullNorUndefined('Block after bad blocks should be defined.', - Blockly.Blocks[BLOCK_TYPE2]); - assertEquals(Object.keys(Blockly.Blocks).length, blockTypeCount + 2); - assertEquals( - 'Expecting 2 warnings, one for each bad block.', warnings.length, 2); - } finally { - delete Blockly.Blocks[BLOCK_TYPE1]; - delete Blockly.Blocks[BLOCK_TYPE2]; - } -} - -/** Ensure message0 creates an input. */ -function test_json_message0() { - var BLOCK_TYPE = 'test_json_message0'; - var MESSAGE0 = 'message0'; - - var workspace = new Blockly.Workspace(); - var block; - try { - Blockly.defineBlocksWithJsonArray([{ - "type": BLOCK_TYPE, - "message0": MESSAGE0 - }]); - - block = new Blockly.Block(workspace, BLOCK_TYPE); - assertEquals(1, block.inputList.length); - assertEquals(1, block.inputList[0].fieldRow.length); - var textField = block.inputList[0].fieldRow[0]; - assertEquals(Blockly.FieldLabel, textField.constructor); - assertEquals(MESSAGE0, textField.getText()); - } finally { - block && block.dispose(); - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE]; - } -} - -/** Ensure message1 creates a new input. */ -function test_json_message1() { - var BLOCK_TYPE = 'test_json_message1'; - var MESSAGE0 = 'message0'; - var MESSAGE1 = 'message1'; - - var workspace = new Blockly.Workspace(); - var block; - try { - Blockly.defineBlocksWithJsonArray([{ - "type": BLOCK_TYPE, - "message0": MESSAGE0, - "message1": MESSAGE1 - }]); - - block = new Blockly.Block(workspace, BLOCK_TYPE); - assertEquals(2, block.inputList.length); - - assertEquals(1, block.inputList[0].fieldRow.length); - var textField = block.inputList[0].fieldRow[0]; - assertEquals(Blockly.FieldLabel, textField.constructor); - assertEquals(MESSAGE0, textField.getText()); - - assertEquals(1, block.inputList[1].fieldRow.length); - var textField = block.inputList[1].fieldRow[0]; - assertEquals(Blockly.FieldLabel, textField.constructor); - assertEquals(MESSAGE1, textField.getText()); - } finally { - block && block.dispose(); - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE]; - } -} - -/** Ensure message string is dereferenced. */ -function test_json_message0_i18n() { - var BLOCK_TYPE = 'test_json_message0_i18n'; - var MESSAGE0 = '%{BKY_MESSAGE}'; - var MESSAGE = 'message'; - - Blockly.Msg['MESSAGE'] = MESSAGE; - - var workspace = new Blockly.Workspace(); - var block; - try { - Blockly.defineBlocksWithJsonArray([{ - "type": BLOCK_TYPE, - "message0": MESSAGE0 - }]); - - block = new Blockly.Block(workspace, BLOCK_TYPE); - assertEquals(1, block.inputList.length); - assertEquals(1, block.inputList[0].fieldRow.length); - var textField = block.inputList[0].fieldRow[0]; - assertEquals(Blockly.FieldLabel, textField.constructor); - assertEquals(MESSAGE, textField.getText()); - } finally { - block && block.dispose(); // Disposes of textField, too. - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE]; - delete Blockly.Msg['MESSAGE']; - } -} - -function test_json_dropdown() { - var BLOCK_TYPE = 'test_json_dropdown'; - var FIELD_NAME = 'FIELD_NAME'; - var LABEL0 = 'LABEL0'; - var VALUE0 = 'VALUE0'; - var LABEL1 = 'LABEL1'; - var VALUE1 = 'VALUE1'; - - var workspace = new Blockly.Workspace(); - var block; - try { - Blockly.defineBlocksWithJsonArray([{ - "type": BLOCK_TYPE, - "message0": "%1", - "args0": [ - { - "type": "field_dropdown", - "name": FIELD_NAME, - "options": [ - [LABEL0, VALUE0], - [LABEL1, VALUE1] - ] - } - ] - }]); - - block = new Blockly.Block(workspace, BLOCK_TYPE); - assertEquals(1, block.inputList.length); - assertEquals(1, block.inputList[0].fieldRow.length); - var dropdown = block.inputList[0].fieldRow[0]; - assertEquals(dropdown, block.getField(FIELD_NAME)); - assertEquals(Blockly.FieldDropdown, dropdown.constructor); - assertEquals(VALUE0, dropdown.getValue()); - - var options = dropdown.getOptions(); - assertEquals(LABEL0, options[0][0]); - assertEquals(VALUE0, options[0][1]); - assertEquals(LABEL1, options[1][0]); - assertEquals(VALUE1, options[1][1]); - } finally { - block && block.dispose(); // Disposes of dropdown, too. - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE]; - } -} - -function test_json_dropdown_image() { - var BLOCK_TYPE = 'test_json_dropdown'; - var FIELD_NAME = 'FIELD_NAME'; - var IMAGE1_ALT_TEXT = 'Localized message.'; - Blockly.Msg['ALT_TEXT'] = IMAGE1_ALT_TEXT; - var IMAGE0 = { - 'width': 12, - 'height': 34, - 'src': 'http://image0.src', - 'alt': 'IMAGE0 alt text' - }; - var VALUE0 = 'VALUE0'; - var IMAGE1 = { - 'width': 56, - 'height': 78, - 'src': 'http://image1.src', - 'alt': '%{BKY_ALT_TEXT}' - }; - var VALUE1 = 'VALUE1'; - var IMAGE2 = { - 'width': 90, - 'height': 123, - 'src': 'http://image2.src' - }; - var VALUE2 = 'VALUE2'; - - var workspace = new Blockly.Workspace(); - var block; - try { - Blockly.defineBlocksWithJsonArray([{ - "type": BLOCK_TYPE, - "message0": "%1", - "args0": [ - { - "type": "field_dropdown", - "name": FIELD_NAME, - "options": [ - [IMAGE0, VALUE0], - [IMAGE1, VALUE1], - [IMAGE2, VALUE2] - ] - } - ] - }]); - - block = new Blockly.Block(workspace, BLOCK_TYPE); - assertEquals(1, block.inputList.length); - assertEquals(1, block.inputList[0].fieldRow.length); - var dropdown = block.inputList[0].fieldRow[0]; - assertEquals(dropdown, block.getField(FIELD_NAME)); - assertEquals(Blockly.FieldDropdown, dropdown.constructor); - assertEquals(VALUE0, dropdown.getValue()); - - var options = dropdown.getOptions(); - var image0 = options[0][0]; - assertEquals(IMAGE0.width, image0.width); - assertEquals(IMAGE0.height, image0.height); - assertEquals(IMAGE0.src, image0.src); - assertEquals(IMAGE0.alt, image0.alt); - assertEquals(VALUE0, options[0][1]); - - var image1 = options[1][0]; - assertEquals(IMAGE1.width, image1.width); - assertEquals(IMAGE1.height, image1.height); - assertEquals(IMAGE1.src, image1.src); - assertEquals(IMAGE1.alt, IMAGE1_ALT_TEXT); // Via Msg reference - assertEquals(VALUE1, options[1][1]); - - var image2 = options[2][0]; - assertEquals(IMAGE2.width, image2.width); - assertEquals(IMAGE2.height, image2.height); - assertEquals(IMAGE2.src, image2.src); - assert(image2.alt == null); // No alt specified. - assertEquals(VALUE2, options[2][1]); - } finally { - block && block.dispose(); // Disposes of dropdown, too. - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE]; - delete Blockly.Msg['ALTTEXT']; - } -} - -function test_defineBlocksWithJsonArray_nullItem() { - var BLOCK_TYPE1 = 'test_block_before_null'; - var BLOCK_TYPE2 = 'test_block_after_null'; - - assertUndefined(Blockly.Blocks[BLOCK_TYPE1]); - assertUndefined(Blockly.Blocks[BLOCK_TYPE2]); - var blockTypeCount = Object.keys(Blockly.Blocks).length; - - try { - var warnings = captureWarnings(function() { - Blockly.defineBlocksWithJsonArray([ - { - "type": BLOCK_TYPE1, - "message0": 'before' - }, - null, - { - "type": BLOCK_TYPE2, - "message0": 'after' - }]); - }); - assertNotNullNorUndefined( - 'Block before null in array should be defined.', - Blockly.Blocks[BLOCK_TYPE1]); - assertNotNullNorUndefined( - 'Block after null in array should be defined.', - Blockly.Blocks[BLOCK_TYPE2]); - assertEquals(Object.keys(Blockly.Blocks).length, blockTypeCount + 2); - assertEquals('Expected 1 warning for the bad block.', warnings.length, 1); - } finally { - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE1]; - delete Blockly.Blocks[BLOCK_TYPE2]; - } -} - -function test_defineBlocksWithJsonArray_undefinedItem() { - var BLOCK_TYPE1 = 'test_block_before_undefined'; - var BLOCK_TYPE2 = 'test_block_after_undefined'; - - assertUndefined(Blockly.Blocks[BLOCK_TYPE1]); - assertUndefined(Blockly.Blocks[BLOCK_TYPE2]); - var blockTypeCount = Object.keys(Blockly.Blocks).length; - - try { - var warnings = captureWarnings(function() { - Blockly.defineBlocksWithJsonArray([ - { - "type": BLOCK_TYPE1, - "message0": 'before' - }, - undefined, - { - "type": BLOCK_TYPE2, - "message0": 'after' - }]); - }); - assertNotNullNorUndefined( - 'Block before undefined in array should be defined.', - Blockly.Blocks[BLOCK_TYPE1]); - assertNotNullNorUndefined( - 'Block after undefined in array should be defined.', - Blockly.Blocks[BLOCK_TYPE2]); - assertEquals(Object.keys(Blockly.Blocks).length, blockTypeCount + 2); - assertEquals('Expected 1 warning for the bad block.', warnings.length, 1); - } finally { - workspace.dispose(); - delete Blockly.Blocks[BLOCK_TYPE1]; - delete Blockly.Blocks[BLOCK_TYPE2]; - } -} diff --git a/tests/jsunit/mocha_jsunit_test_runner.js b/tests/jsunit/mocha_jsunit_test_runner.js deleted file mode 100644 index 91a8e81c2..000000000 --- a/tests/jsunit/mocha_jsunit_test_runner.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview A mocha test runner that mimics the behaviour of - * goog.testing.junit - * @author samelh@google.com (Sam El-Husseini) - */ -'use strict'; - - -/** - * Setup mocha - */ -mocha.setup({ - ui: 'tdd' -}); -// Add mocha div -var mochaDiv = document.createElement('div'); -mochaDiv.id = 'mocha'; -document.body.appendChild(mochaDiv); -var mochaCss = document.createElement('link'); -mochaCss.setAttribute('href', 'https://unpkg.com/mocha@5.2.0/mocha.css'); -mochaCss.setAttribute('rel', 'stylesheet'); -document.head.appendChild(mochaCss); - - -/** - * Begin by discovering all of the test methods, test methods are any - * function on the window object that begins with the prefix `test` - */ -var allMethods = Object.getOwnPropertyNames(window); -var allTests = []; -for (var i = 0, method; i < allMethods.length, method = allMethods[i]; i++) { - if (method.indexOf('test') === 0 && method !== 'test' && - typeof window[method] === 'function') { - allTests.push(method); - } -} - - -/** - * Split test methods into various suites by grouping them based on the - * test name. Tests the begin with the same prefix are grouped together - * into a suite. - */ -var suites = {}; -for (var i = 0, method; i < allTests.length, method = allTests[i]; i++) { - var testName = method.substr(5); - var underscore = testName.indexOf('_'); - var suiteName = underscore > -1 ? testName.substr(0, underscore) : 'test'; - if (!suites.hasOwnProperty(suiteName)) { - suites[suiteName] = []; - } - suites[suiteName].push(method); -} - - -/** - * Setup chai fail method - */ -function fail() { - chai.fail(); -} - - -/** - * Wrap all unit tests into mocha test cases. Slot them into the different - * suite groups that we found. - */ -suite('jsunit tests', function() { - for (var i = 0, suiteKeys = Object.keys(suites), suiteName; - i < suiteKeys.length, suiteName = suiteKeys[i]; i++) { - suite(suiteName, function() { - for (var j = 0, tests = suites[suiteName], method; - j < tests.length, method = tests[j]; j++) { - test(method, function() { - window[this.test.title](); - }); - } - }); - } -}); - - -/** - * Create a div for failure results, and run the mocha tests. - */ -var failureDiv = document.createElement('div'); -failureDiv.id = 'failureCount'; -failureDiv.style = 'display:none'; -failureDiv.setAttribute('tests_failed', 'unset'); -document.body.appendChild(failureDiv); -mocha.run(function(failures) { - failureDiv.setAttribute('tests_failed', failures); -}); diff --git a/tests/jsunit/run_jsunit_tests_in_browser.js b/tests/jsunit/run_jsunit_tests_in_browser.js deleted file mode 100644 index fd291cae5..000000000 --- a/tests/jsunit/run_jsunit_tests_in_browser.js +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Node.js script to run JsUnit tests in Chrome, via webdriver. - */ -var webdriverio = require('webdriverio'); - -module.exports = runJsUnitTestsInBrowser; - -/** - * Runs the JsUnit tests in this directory in Chrome. It uses webdriverio to - * launch Chrome and load index.html. Outputs a summary of the test results - * to the console. - * @return 0 on success, 1 on failure. - */ -async function runJsUnitTestsInBrowser() { - var options = { - capabilities: { - browserName: 'chrome' - } - }; - // Run in headless mode on Travis. - if (process.env.TRAVIS_CI) { - options.capabilities['goog:chromeOptions'] = { - args: ['--headless', '--no-sandbox', '--disable-dev-shm-usage'] - }; - } - - var url = 'file://' + __dirname + '/index.html'; - console.log('Starting webdriverio...'); - const browser = await webdriverio.remote(options); - console.log('Initialized.\nLoading url: ' + url); - await browser.url(url); - - await browser.waitUntil(async () => { - var elem = await browser.$('#failureCount'); - var text = await elem.getAttribute('tests_failed'); - return text != 'unset'; - }, 6000); - - const elem = await browser.$('#failureCount'); - const numOfFailure = await elem.getAttribute('tests_failed'); - - console.log('============Blockly Unit Test Summary================='); - console.log(numOfFailure); - console.log(numOfFailure + ' tests failed'); - console.log('============Blockly Unit Test Summary================='); - if (parseInt(numOfFailure) !== 0) { - await browser.deleteSession(); - return 1; - } - await browser.deleteSession(); - return 0; -} - -if (require.main === module) { - runJsUnitTestsInBrowser().catch(e => { - console.error(e); - process.exit(1); - }).then(function(result) { - if (result) { - console.log('JSUnit tests failed'); - process.exit(1); - } else { - console.log('JSUnit tests passed'); - process.exit(0); - } - }); -} diff --git a/tests/jsunit/test_utilities.js b/tests/jsunit/test_utilities.js deleted file mode 100644 index 81f78b23c..000000000 --- a/tests/jsunit/test_utilities.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - - /** - * @fileoverview Test utilities. - * @author marisaleung@google.com (Marisa Leung) - */ -'use strict'; - - -/** - * The normal blockly event fire function. We sometimes override this. This - * handle lets us reset after an override. - */ -var savedFireFunc = Blockly.Events.fire; - -/** - * A helper function to replace Blockly.Events.fire in tests. - */ -function temporary_fireEvent(event) { - if (!Blockly.Events.isEnabled()) { - return; - } - Blockly.Events.FIRE_QUEUE_.push(event); - Blockly.Events.fireNow_(); -} - -/** - * Check that two arrays have the same content. - * @param {!Array.} array1 The first array. - * @param {!Array.} array2 The second array. - */ -function isEqualArrays(array1, array2) { - assertEquals(array1.length, array2.length); - for (var i = 0; i < array1.length; i++) { - assertEquals(array1[i], array2[i]); - } -} - -/** - * Creates a new method stub. Sets the expected return values and - * the parameters if any exist. - * @param {!Object} scope The scope of the method to be mocked out. - * @param {!string} funcName The name of the function we're going to mock. - * @param {Array.} parameters The parameters to call the mock with. - * @param {Array.} return_values The values to return when called. - * @return {!sinon.SinonStub} The stub method. - */ -function setUpMockMethod(scope, funcName, parameters, return_values) { - var stub = sinon.stub(scope, funcName); - if (return_values) { - for (var i = 0, return_value; return_value = return_values[i]; i++) { - if (parameters && i < parameters.length) { - stub(parameters[i]).returns(return_value); - } - else { - stub.onCall(i).returns(return_value); - } - } - } - // If there are no return values but there are parameters, we are only - // recording specific method calls. - else if (parameters) { - for (var i = 0; i < parameters.length; i++) { - stub(parameters[i]); - } - } - return stub; -} - -/** - * Check if a variable with the given values exists. - * @param {Blockly.Workspace|Blockly.VariableMap} container The workspace or - * variableMap the checked variable belongs to. - * @param {!string} name The expected name of the variable. - * @param {!string} type The expected type of the variable. - * @param {!string} id The expected id of the variable. - */ -function checkVariableValues(container, name, type, id) { - var variable = container.getVariableById(id); - assertNotUndefined(variable); - assertEquals(name, variable.name); - assertEquals(type, variable.type); - assertEquals(id, variable.getId()); -} - -/** - * Create a test get_var_block. - * Will fail if get_var_block isn't defined. - * @param {!Blockly.Workspace} workspace The workspace on which to create the - * block. - * @param {!string} variable_id The id of the variable to reference. - * @return {!Blockly.Block} The created block. - */ -function createMockVarBlock(workspace, variable_id) { - if (!Blockly.Blocks['get_var_block']) { - fail(); - } - // Turn off events to avoid testing XML at the same time. - Blockly.Events.disable(); - var block = new Blockly.Block(workspace, 'get_var_block'); - block.inputList[0].fieldRow[0].setValue(variable_id); - Blockly.Events.enable(); - return block; -} - -function createTwoVariablesAndBlocks(workspace) { - // Create two variables of different types. - workspace.createVariable('name1', 'type1', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); - // Create blocks to refer to both of them. - createMockVarBlock(workspace, 'id1'); - createMockVarBlock(workspace, 'id2'); -} - -function createVariableAndBlock(workspace) { - workspace.createVariable('name1', 'type1', 'id1'); - createMockVarBlock(workspace, 'id1'); -} - -function defineGetVarBlock() { - Blockly.defineBlocksWithJsonArray([{ - "type": "get_var_block", - "message0": "%1", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - "variableTypes": ["", "type1", "type2"] - } - ] - }]); -} - -function undefineGetVarBlock() { - delete Blockly.Blocks['get_var_block']; -} - -/** - * Capture the strings sent to console.warn() when calling a function. - * @param {function} innerFunc The function where warnings may called. - * @return {string[]} The warning messages (only the first arguments). - */ -function captureWarnings(innerFunc) { - var msgs = []; - var nativeConsoleWarn = console.warn; - try { - console.warn = function(msg) { - msgs.push(msg); - nativeConsoleWarn.apply(console, arguments); - }; - innerFunc(); - } finally { - console.warn = nativeConsoleWarn; - } - return msgs; -} diff --git a/tests/jsunit/utils_dom_test.js b/tests/jsunit/utils_dom_test.js deleted file mode 100644 index 90c58a8a8..000000000 --- a/tests/jsunit/utils_dom_test.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -function test_addClass() { - var p = document.createElement('p'); - Blockly.utils.dom.addClass(p, 'one'); - assertEquals('Adding "one"', 'one', p.className); - Blockly.utils.dom.addClass(p, 'one'); - assertEquals('Adding duplicate "one"', 'one', p.className); - Blockly.utils.dom.addClass(p, 'two'); - assertEquals('Adding "two"', 'one two', p.className); - Blockly.utils.dom.addClass(p, 'two'); - assertEquals('Adding duplicate "two"', 'one two', p.className); - Blockly.utils.dom.addClass(p, 'three'); - assertEquals('Adding "three"', 'one two three', p.className); -} - -function test_hasClass() { - var p = document.createElement('p'); - p.className = ' one three two three '; - assertTrue('Has "one"', Blockly.utils.dom.hasClass(p, 'one')); - assertTrue('Has "two"', Blockly.utils.dom.hasClass(p, 'two')); - assertTrue('Has "three"', Blockly.utils.dom.hasClass(p, 'three')); - assertFalse('Has no "four"', Blockly.utils.dom.hasClass(p, 'four')); - assertFalse('Has no "t"', Blockly.utils.dom.hasClass(p, 't')); - } - -function test_removeClass() { - var p = document.createElement('p'); - p.className = ' one three two three '; - Blockly.utils.dom.removeClass(p, 'two'); - assertEquals('Removing "two"', 'one three three', p.className); - Blockly.utils.dom.removeClass(p, 'four'); - assertEquals('Removing "four"', 'one three three', p.className); - Blockly.utils.dom.removeClass(p, 'three'); - assertEquals('Removing "three"', 'one', p.className); - Blockly.utils.dom.removeClass(p, 'ne'); - assertEquals('Removing "ne"', 'one', p.className); - Blockly.utils.dom.removeClass(p, 'one'); - assertEquals('Removing "one"', '', p.className); - Blockly.utils.dom.removeClass(p, 'zero'); - assertEquals('Removing "zero"', '', p.className); -} diff --git a/tests/jsunit/utils_math_test.js b/tests/jsunit/utils_math_test.js deleted file mode 100644 index 6f56ec8b1..000000000 --- a/tests/jsunit/utils_math_test.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -function test_toRadians() { - var quarter = Math.PI / 2; - assertEquals('-90', -quarter, Blockly.utils.math.toRadians(-90)); - assertEquals('0', 0, Blockly.utils.math.toRadians(0)); - assertEquals('90', quarter, Blockly.utils.math.toRadians(90)); - assertEquals('180', 2 * quarter, Blockly.utils.math.toRadians(180)); - assertEquals('270', 3 * quarter, Blockly.utils.math.toRadians(270)); - assertEquals('360', 4 * quarter, Blockly.utils.math.toRadians(360)); - assertEquals('450', 5 * quarter, Blockly.utils.math.toRadians(360 + 90)); -} - -function test_toDegrees() { - var quarter = Math.PI / 2; - assertEquals('-90', -90, Blockly.utils.math.toDegrees(-quarter)); - assertEquals('0', 0, Blockly.utils.math.toDegrees(0)); - assertEquals('90', 90, Blockly.utils.math.toDegrees(quarter)); - assertEquals('180', 180, Blockly.utils.math.toDegrees(2 * quarter)); - assertEquals('270', 270, Blockly.utils.math.toDegrees(3 * quarter)); - assertEquals('360', 360, Blockly.utils.math.toDegrees(4 * quarter)); - assertEquals('450', 360 + 90, Blockly.utils.math.toDegrees(5 * quarter)); -} diff --git a/tests/jsunit/utils_string_test.js b/tests/jsunit/utils_string_test.js deleted file mode 100644 index 507368b8b..000000000 --- a/tests/jsunit/utils_string_test.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -function test_startsWith() { - assertEquals('Does not start with', false, Blockly.utils.string.startsWith('123', '2')); - assertEquals('Start with', true, Blockly.utils.string.startsWith('123', '12')); - assertEquals('Start with empty string 1', true, Blockly.utils.string.startsWith('123', '')); - assertEquals('Start with empty string 2', true, Blockly.utils.string.startsWith('', '')); -} - -function test_commonWordPrefix() { - var len = Blockly.utils.string.commonWordPrefix('one,two,three,four,five'.split(',')); - assertEquals('No prefix', 0, len); - len = Blockly.utils.string.commonWordPrefix('Xone,Xtwo,Xthree,Xfour,Xfive'.split(',')); - assertEquals('No word prefix', 0, len); - len = Blockly.utils.string.commonWordPrefix('abc de,abc de,abc de,abc de'.split(',')); - assertEquals('Full equality', 6, len); - len = Blockly.utils.string.commonWordPrefix('abc deX,abc deY'.split(',')); - assertEquals('One word prefix', 4, len); - len = Blockly.utils.string.commonWordPrefix('abc de,abc deY'.split(',')); - assertEquals('Overflow no', 4, len); - len = Blockly.utils.string.commonWordPrefix('abc de,abc de Y'.split(',')); - assertEquals('Overflow yes', 6, len); - len = Blockly.utils.string.commonWordPrefix(['Hello World']); - assertEquals('List of one', 11, len); - len = Blockly.utils.string.commonWordPrefix([]); - assertEquals('Empty list', 0, len); - len = Blockly.utils.string.commonWordPrefix('turn left,turn right'.split(',')); - assertEquals('No prefix due to &nbsp;', 0, len); - len = Blockly.utils.string.commonWordPrefix('turn\u00A0left,turn\u00A0right'.split(',')); - assertEquals('No prefix due to \\u00A0', 0, len); -} - -function test_commonWordSuffix() { - var len = Blockly.utils.string.commonWordSuffix('one,two,three,four,five'.split(',')); - assertEquals('No prefix', 0, len); - len = Blockly.utils.string.commonWordSuffix('oneX,twoX,threeX,fourX,fiveX'.split(',')); - assertEquals('No word prefix', 0, len); - len = Blockly.utils.string.commonWordSuffix('abc de,abc de,abc de,abc de'.split(',')); - assertEquals('Full equality', 6, len); - len = Blockly.utils.string.commonWordSuffix('Xabc de,Yabc de'.split(',')); - assertEquals('One word prefix', 3, len); - len = Blockly.utils.string.commonWordSuffix('abc de,Yabc de'.split(',')); - assertEquals('Overflow no', 3, len); - len = Blockly.utils.string.commonWordSuffix('abc de,Y abc de'.split(',')); - assertEquals('Overflow yes', 6, len); - len = Blockly.utils.string.commonWordSuffix(['Hello World']); - assertEquals('List of one', 11, len); - len = Blockly.utils.string.commonWordSuffix([]); - assertEquals('Empty list', 0, len); -} - -function test_shortestStringLength() { - var len = Blockly.utils.string.shortestStringLength('one,two,three,four,five'.split(',')); - assertEquals('Length of "one"', 3, len); - len = Blockly.utils.string.shortestStringLength('one,two,three,four,five,'.split(',')); - assertEquals('Length of ""', 0, len); - len = Blockly.utils.string.shortestStringLength(['Hello World']); - assertEquals('List of one', 11, len); - len = Blockly.utils.string.shortestStringLength([]); - assertEquals('Empty list', 0, len); -} - diff --git a/tests/jsunit/utils_test.js b/tests/jsunit/utils_test.js deleted file mode 100644 index 6e701a385..000000000 --- a/tests/jsunit/utils_test.js +++ /dev/null @@ -1,210 +0,0 @@ -/** - * @license - * Copyright 2011 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -function test_genUid() { - var uuids = {}; - for (var i = 0; i < 1000; i++) { - var uuid = Blockly.utils.genUid(); - assertFalse('UUID different: ' + uuid, uuid in uuids); - uuids[uuid] = true; - } -} - -function test_tokenizeInterpolation() { - var tokens = Blockly.utils.tokenizeInterpolation(''); - assertArrayEquals('Null interpolation', [], tokens); - - tokens = Blockly.utils.tokenizeInterpolation('Hello'); - assertArrayEquals('No interpolation', ['Hello'], tokens); - - tokens = Blockly.utils.tokenizeInterpolation('Hello%World'); - assertArrayEquals('Unescaped %.', ['Hello%World'], tokens); - - tokens = Blockly.utils.tokenizeInterpolation('Hello%%World'); - assertArrayEquals('Escaped %.', ['Hello%World'], tokens); - - tokens = Blockly.utils.tokenizeInterpolation('Hello %1 World'); - assertArrayEquals('Interpolation.', ['Hello ', 1, ' World'], tokens); - - tokens = Blockly.utils.tokenizeInterpolation('%123Hello%456World%789'); - assertArrayEquals('Interpolations.', [123, 'Hello', 456, 'World', 789], tokens); - - tokens = Blockly.utils.tokenizeInterpolation('%%%x%%0%00%01%'); - assertArrayEquals('Torture interpolations.', ['%%x%0', 0, 1, '%'], tokens); - - Blockly.Msg = Blockly.Msg || {}; - - Blockly.Msg.STRING_REF = 'test string'; - tokens = Blockly.utils.tokenizeInterpolation('%{bky_string_ref}'); - assertArrayEquals('String table reference, lowercase', ['test string'], tokens); - tokens = Blockly.utils.tokenizeInterpolation('%{BKY_STRING_REF}'); - assertArrayEquals('String table reference, uppercase', ['test string'], tokens); - - Blockly.Msg.WITH_PARAM = 'before %1 after'; - tokens = Blockly.utils.tokenizeInterpolation('%{bky_with_param}'); - assertArrayEquals('String table reference, with parameter', ['before ', 1, ' after'], tokens); - - Blockly.Msg.RECURSE = 'before %{bky_string_ref} after'; - tokens = Blockly.utils.tokenizeInterpolation('%{bky_recurse}'); - assertArrayEquals('String table reference, with subreference', ['before test string after'], tokens); - - // Error cases... - tokens = Blockly.utils.tokenizeInterpolation('%{bky_undefined}'); - assertArrayEquals('Undefined string table reference', ['%{bky_undefined}'], tokens); - - Blockly.Msg['1'] = 'Will not match'; - tokens = Blockly.utils.tokenizeInterpolation('before %{1} after'); - assertArrayEquals('Invalid initial digit in string table reference', ['before %{1} after'], tokens); - - Blockly.Msg['TWO WORDS'] = 'Will not match'; - tokens = Blockly.utils.tokenizeInterpolation('before %{two words} after'); - assertArrayEquals('Invalid character in string table reference: space', ['before %{two words} after'], tokens); - - Blockly.Msg['TWO-WORDS'] = 'Will not match'; - tokens = Blockly.utils.tokenizeInterpolation('before %{two-words} after'); - assertArrayEquals('Invalid character in string table reference: dash', ['before %{two-words} after'], tokens); - - Blockly.Msg['TWO.WORDS'] = 'Will not match'; - tokens = Blockly.utils.tokenizeInterpolation('before %{two.words} after'); - assertArrayEquals('Invalid character in string table reference: period', ['before %{two.words} after'], tokens); - - Blockly.Msg['AB&C'] = 'Will not match'; - tokens = Blockly.utils.tokenizeInterpolation('before %{ab&c} after'); - assertArrayEquals('Invalid character in string table reference: &', ['before %{ab&c} after'], tokens); - - Blockly.Msg['UNCLOSED'] = 'Will not match'; - tokens = Blockly.utils.tokenizeInterpolation('before %{unclosed'); - assertArrayEquals('String table reference, with parameter', ['before %{unclosed'], tokens); -} - -function test_replaceMessageReferences() { - Blockly.Msg = Blockly.Msg || {}; - Blockly.Msg.STRING_REF = 'test string'; - Blockly.Msg.SUBREF = 'subref'; - Blockly.Msg.STRING_REF_WITH_ARG = 'test %1 string'; - Blockly.Msg.STRING_REF_WITH_SUBREF = 'test %{bky_subref} string'; - - var resultString = Blockly.utils.replaceMessageReferences(''); - assertEquals('Empty string produces empty string', '', resultString); - - resultString = Blockly.utils.replaceMessageReferences('%%'); - assertEquals('Escaped %', '%', resultString); - resultString = Blockly.utils.replaceMessageReferences('%%{bky_string_ref}'); - assertEquals('Escaped %', '%{bky_string_ref}', resultString); - - resultString = Blockly.utils.replaceMessageReferences('%a'); - assertEquals('Unrecognized % escape code treated as literal', '%a', resultString); - - resultString = Blockly.utils.replaceMessageReferences('%1'); - assertEquals('Interpolation tokens ignored.', '%1', resultString); - resultString = Blockly.utils.replaceMessageReferences('%1 %2'); - assertEquals('Interpolation tokens ignored.', '%1 %2', resultString); - resultString = Blockly.utils.replaceMessageReferences('before %1 after'); - assertEquals('Interpolation tokens ignored.', 'before %1 after', resultString); - - // Blockly.Msg.STRING_REF cases: - resultString = Blockly.utils.replaceMessageReferences('%{bky_string_ref}'); - assertEquals('Message ref dereferenced.', 'test string', resultString); - resultString = Blockly.utils.replaceMessageReferences('before %{bky_string_ref} after'); - assertEquals('Message ref dereferenced.', 'before test string after', resultString); - - // Blockly.Msg.STRING_REF_WITH_ARG cases: - resultString = Blockly.utils.replaceMessageReferences('%{bky_string_ref_with_arg}'); - assertEquals('Message ref dereferenced with argument preserved.', 'test %1 string', resultString); - resultString = Blockly.utils.replaceMessageReferences('before %{bky_string_ref_with_arg} after'); - assertEquals('Message ref dereferenced with argument preserved.', 'before test %1 string after', resultString); - - // Blockly.Msg.STRING_REF_WITH_SUBREF cases: - resultString = Blockly.utils.replaceMessageReferences('%{bky_string_ref_with_subref}'); - assertEquals('Message ref and subref dereferenced.', 'test subref string', resultString); - resultString = Blockly.utils.replaceMessageReferences('before %{bky_string_ref_with_subref} after'); - assertEquals('Message ref and subref dereferenced.', 'before test subref string after', resultString); -} - -function test_arrayRemove() { - var arr = [1, 2, 3, 2]; - assertEquals('Remove Not found', false, Blockly.utils.arrayRemove(arr, 0)); - assertEquals('Remove Not found result', '1,2,3,2', arr.join(',')); - assertEquals('Remove item', true, Blockly.utils.arrayRemove(arr, 2)); - assertEquals('Remove item result', '1,3,2', arr.join(',')); - assertEquals('Remove item again', true, Blockly.utils.arrayRemove(arr, 2)); - assertEquals('Remove item again result', '1,3', arr.join(',')); -} - -function test_XY_REGEX() { - var regex = Blockly.utils.getRelativeXY.XY_REGEX_; - var m; - m = 'INVALID'.match(regex); - assertNull(m); - - m = 'translate(10)'.match(regex); - assertEquals('translate(10), x', '10', m[1]); - assertUndefined('translate(10), y', m[3]); - - m = 'translate(11, 12)'.match(regex); - assertEquals('translate(11, 12), x', '11', m[1]); - assertEquals('translate(11, 12), y', '12', m[3]); - - m = 'translate(13,14)'.match(regex); - assertEquals('translate(13,14), x', '13', m[1]); - assertEquals('translate(13,14), y', '14', m[3]); - - m = 'translate(15 16)'.match(regex); - assertEquals('translate(15 16), x', '15', m[1]); - assertEquals('translate(15 16), y', '16', m[3]); - - m = 'translate(1.23456e+42 0.123456e-42)'.match(regex); - assertEquals('translate(1.23456e+42 0.123456e-42), x', '1.23456e+42', m[1]); - assertEquals('translate(1.23456e+42 0.123456e-42), y', '0.123456e-42', m[3]); -} - -function XY_STYLE_REGEX_() { - var regex = Blockly.utils.getRelativeXY.XY_STYLE_REGEX_; - var m; - m = 'INVALID'.match(regex); - assertNull(m); - - m = 'transform:translate(9px)'.match(regex); - assertEquals('transform:translate(9px), x', '9', m[1]); - assertUndefined('transform:translate(9px), y', m[3]); - - m = 'transform:translate3d(10px)'.match(regex); - assertEquals('transform:translate3d(10px), x', '10', m[1]); - assertUndefined('transform:translate(10px), y', m[3]); - - m = 'transform: translate(11px, 12px)'.match(regex); - assertEquals('transform: translate(11px, 12px), x', '11', m[1]); - assertEquals('transform: translate(11px, 12px), y', '12', m[3]); - - m = 'transform: translate(13px,14px)'.match(regex); - assertEquals('transform: translate(13px,14px), x', '13', m[1]); - assertEquals('transform: translate(13px,14px), y', '14', m[3]); - - m = 'transform: translate(15px 16px)'.match(regex); - assertEquals('transform: translate(15px 16px), x', '15', m[1]); - assertEquals('transform: translate(15px 16px), y', '16', m[3]); - - m = 'transform: translate(1.23456e+42px 0.123456e-42px)'.match(regex); - assertEquals('transform: translate(1.23456e+42px 0.123456e-42px), x', '1.23456e+42', m[1]); - assertEquals('transform: translate(1.23456e+42px 0.123456e-42px), y', '0.123456e-42', m[3]); - - m = 'transform:translate3d(20px, 21px, 22px)'.match(regex); - assertEquals('transform:translate3d(20px, 21px, 22px), x', '21', m[1]); - assertEquals('transform:translate3d(20px, 21px, 22px), y', '22', m[3]); - - m = 'transform:translate3d(23px,24px,25px)'.match(regex); - assertEquals('transform:translate3d(23px,24px,25px), x', '23', m[1]); - assertEquals('transform:translate3d(23px,24px,25px), y', '24', m[3]); - - m = 'transform:translate3d(26px 27px 28px)'.match(regex); - assertEquals('transform:translate3d(26px 27px 28px), x', '26', m[1]); - assertEquals('transform:translate3d(26px 27px 28px), y', '27', m[3]); - - m = 'transform:translate3d(1.23456e+42px 0.123456e-42px 42px)'.match(regex); - assertEquals('transform:translate3d(1.23456e+42px 0.123456e-42px 42px), x', '1.23456e+42', m[1]); - assertEquals('transform:translate3d(1.23456e+42px 0.123456e-42px 42px), y', '0.123456e-42', m[3]); -} diff --git a/tests/jsunit/variable_map_test.js b/tests/jsunit/variable_map_test.js deleted file mode 100644 index 8f5b2f6ff..000000000 --- a/tests/jsunit/variable_map_test.js +++ /dev/null @@ -1,284 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - - /** - * @fileoverview Tests for variable map. - * @author marisaleung@google.com (Marisa Leung) - */ -'use strict'; - -var variable_map; -var mockControl_; -var workspace; - -function variableMapTest_setUp() { - workspace = new Blockly.Workspace(); - variable_map = new Blockly.VariableMap(workspace); -} - -function variableMapTest_tearDown() { - workspace.dispose(); - if (mockControl_) { - mockControl_.restore(); - } - variable_map = null; -} - -function test_getVariable_ByNameAndType() { - variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', 'type1', 'id1'); - var var_2 = variable_map.createVariable('name2', 'type1', 'id2'); - var var_3 = variable_map.createVariable('name3', 'type2', 'id3'); - var result_1 = variable_map.getVariable('name1', 'type1'); - var result_2 = variable_map.getVariable('name2', 'type1'); - var result_3 = variable_map.getVariable('name3', 'type2'); - - // Searching by name + type is correct. - assertEquals(var_1, result_1); - assertEquals(var_2, result_2); - assertEquals(var_3, result_3); - - // Searching only by name defaults to the '' type. - assertNull(variable_map.getVariable('name1')); - assertNull(variable_map.getVariable('name2')); - assertNull(variable_map.getVariable('name3')); - variableMapTest_tearDown(); -} - -function test_getVariable_NotFound() { - variableMapTest_setUp(); - var result = variable_map.getVariable('name1'); - assertNull(result); - variableMapTest_tearDown(); -} - -function test_getVariableById_Trivial() { - variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', 'type1', 'id1'); - var var_2 = variable_map.createVariable('name2', 'type1', 'id2'); - var var_3 = variable_map.createVariable('name3', 'type2', 'id3'); - var result_1 = variable_map.getVariableById('id1'); - var result_2 = variable_map.getVariableById('id2'); - var result_3 = variable_map.getVariableById('id3'); - - assertEquals(var_1, result_1); - assertEquals(var_2, result_2); - assertEquals(var_3, result_3); - variableMapTest_tearDown(); -} - -function test_getVariableById_NotFound() { - variableMapTest_setUp(); - var result = variable_map.getVariableById('id1'); - assertNull(result); - variableMapTest_tearDown(); -} - -function test_createVariableTrivial() { - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - checkVariableValues(variable_map, 'name1', 'type1', 'id1'); - variableMapTest_tearDown(); -} - -function test_createVariableAlreadyExists() { - // Expect that when the variable already exists, the variableMap_ is unchanged. - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - - // Assert there is only one variable in the variable_map. - var keys = Object.keys(variable_map.variableMap_); - assertEquals(1, keys.length); - var varMapLength = variable_map.variableMap_[keys[0]].length; - assertEquals(1, varMapLength); - - variable_map.createVariable('name1', 'type1'); - checkVariableValues(variable_map, 'name1', 'type1', 'id1'); - // Check that the size of the variableMap_ did not change. - keys = Object.keys(variable_map.variableMap_); - assertEquals(1, keys.length); - varMapLength = variable_map.variableMap_[keys[0]].length; - assertEquals(1, varMapLength); - variableMapTest_tearDown(); -} - -function test_createVariableNameAlreadyExists() { - // Expect that when a variable with the same name but a different type already - // exists, the new variable is created. - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - - // Assert there is only one variable in the variable_map. - var keys = Object.keys(variable_map.variableMap_); - assertEquals(1, keys.length); - var varMapLength = variable_map.variableMap_[keys[0]].length; - assertEquals(1, varMapLength); - - variable_map.createVariable('name1', 'type2', 'id2'); - checkVariableValues(variable_map, 'name1', 'type1', 'id1'); - checkVariableValues(variable_map, 'name1', 'type2', 'id2'); - // Check that the size of the variableMap_ did change. - keys = Object.keys(variable_map.variableMap_); - assertEquals(2, keys.length); - variableMapTest_tearDown(); -} -function test_createVariableNullAndUndefinedType() { - variableMapTest_setUp(); - variable_map.createVariable('name1', null, 'id1'); - variable_map.createVariable('name2', undefined, 'id2'); - - checkVariableValues(variable_map, 'name1', '', 'id1'); - checkVariableValues(variable_map, 'name2', '', 'id2'); - variableMapTest_tearDown(); -} - -function test_createVariableNullId() { - variableMapTest_setUp(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2']); - try { - variable_map.createVariable('name1', 'type1', null); - checkVariableValues(variable_map, 'name1', 'type1', '1'); - } finally { - variableMapTest_tearDown(); - } -} - -function test_createVariableUndefinedId() { - variableMapTest_setUp(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '2']); - try { - variable_map.createVariable('name1', 'type1', undefined); - checkVariableValues(variable_map, 'name1', 'type1', '1'); - } finally { - variableMapTest_tearDown(); - } -} - -function test_createVariableIdAlreadyExists() { - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - try { - variable_map.createVariable('name2', 'type2', 'id1'); - fail(); - } catch (e) { - // expected - } - variableMapTest_tearDown(); -} - -function test_createVariableMismatchedIdAndType() { - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - try { - variable_map.createVariable('name1', 'type2', 'id1'); - fail(); - } catch (e) { - // expected - } - try { - variable_map.createVariable('name1', 'type1', 'id2'); - fail(); - } catch (e) { - // expected - } - variableMapTest_tearDown(); -} - -function test_createVariableTwoSameTypes() { - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - variable_map.createVariable('name2', 'type1', 'id2'); - - checkVariableValues(variable_map, 'name1', 'type1', 'id1'); - checkVariableValues(variable_map, 'name2', 'type1', 'id2'); - variableMapTest_tearDown(); -} - -function test_getVariablesOfType_Trivial() { - variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', 'type1', 'id1'); - var var_2 = variable_map.createVariable('name2', 'type1', 'id2'); - variable_map.createVariable('name3', 'type2', 'id3'); - variable_map.createVariable('name4', 'type3', 'id4'); - var result_array_1 = variable_map.getVariablesOfType('type1'); - var result_array_2 = variable_map.getVariablesOfType('type5'); - isEqualArrays([var_1, var_2], result_array_1); - isEqualArrays([], result_array_2); - variableMapTest_tearDown(); -} - -function test_getVariablesOfType_Null() { - variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', '', 'id1'); - var var_2 = variable_map.createVariable('name2', '', 'id2'); - var var_3 = variable_map.createVariable('name3', '', 'id3'); - variable_map.createVariable('name4', 'type1', 'id4'); - var result_array = variable_map.getVariablesOfType(null); - isEqualArrays([var_1, var_2, var_3], result_array); - variableMapTest_tearDown(); -} - -function test_getVariablesOfType_EmptyString() { - variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', null, 'id1'); - var var_2 = variable_map.createVariable('name2', null, 'id2'); - var result_array = variable_map.getVariablesOfType(''); - isEqualArrays([var_1, var_2], result_array); - variableMapTest_tearDown(); -} - -function test_getVariablesOfType_Deleted() { - variableMapTest_setUp(); - var variable = variable_map.createVariable('name1', null, 'id1'); - variable_map.deleteVariable(variable); - var result_array = variable_map.getVariablesOfType(''); - isEqualArrays([], result_array); - variableMapTest_tearDown(); -} - -function test_getVariablesOfType_DoesNotExist() { - variableMapTest_setUp(); - var result_array = variable_map.getVariablesOfType('type1'); - isEqualArrays([], result_array); - variableMapTest_tearDown(); -} - -function test_getVariableTypes_Trivial() { - variableMapTest_setUp(); - variable_map.createVariable('name1', 'type1', 'id1'); - variable_map.createVariable('name2', 'type1', 'id2'); - variable_map.createVariable('name3', 'type2', 'id3'); - variable_map.createVariable('name4', 'type3', 'id4'); - var result_array = variable_map.getVariableTypes(); - // The empty string is always an option. - isEqualArrays(['type1', 'type2', 'type3', ''], result_array); - variableMapTest_tearDown(); -} - -function test_getVariableTypes_None() { - variableMapTest_setUp(); - // The empty string is always an option. - var result_array = variable_map.getVariableTypes(); - isEqualArrays([''], result_array); - variableMapTest_tearDown(); -} - -function test_getAllVariables_Trivial() { - variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', 'type1', 'id1'); - var var_2 = variable_map.createVariable('name2', 'type1', 'id2'); - var var_3 = variable_map.createVariable('name3', 'type2', 'id3'); - var result_array = variable_map.getAllVariables(); - isEqualArrays([var_1, var_2, var_3], result_array); - variableMapTest_tearDown(); -} - -function test_getAllVariables_None() { - variableMapTest_setUp(); - var result_array = variable_map.getAllVariables(); - isEqualArrays([], result_array); - variableMapTest_tearDown(); -} diff --git a/tests/jsunit/variable_model_test.js b/tests/jsunit/variable_model_test.js deleted file mode 100644 index 58e32c9b6..000000000 --- a/tests/jsunit/variable_model_test.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Tests for variable model. - * @author marisaleung@google.com (Marisa Leung) - */ -'use strict'; - -var variable; -var workspace; - -function variableModelTest_setUp() { - workspace = new Blockly.Workspace(); -} - -function variableModelTest_tearDown() { - workspace.dispose(); - variable = null; -} - -/** - * These tests check the constructor of the variable model. - */ -function testInit_Trivial() { - variableModelTest_setUp(); - variable = new Blockly.VariableModel(workspace, 'test', 'test_type', - 'test_id'); - assertEquals('test', variable.name); - assertEquals('test_type', variable.type); - assertEquals('test_id', variable.id_); - variableModelTest_tearDown(); -} - -function testInit_NullType() { - variableModelTest_setUp(); - variable = new Blockly.VariableModel(workspace, 'test', null, 'test_id'); - assertEquals('', variable.type); - variableModelTest_tearDown(); -} - -function testInit_UndefinedType() { - variableModelTest_setUp(); - variable = new Blockly.VariableModel(workspace, 'test', undefined, 'test_id'); - assertEquals('', variable.type); - variableModelTest_tearDown(); -} - -function testInit_NullId() { - variableModelTest_setUp(); - variable = new Blockly.VariableModel(workspace, 'test', 'test_type', null); - assertEquals('test', variable.name); - assertEquals('test_type', variable.type); - assertNotNull(variable.id_); - variableModelTest_tearDown(); -} - -function testInit_UndefinedId() { - variableModelTest_setUp(); - variable = new Blockly.VariableModel(workspace, 'test', 'test_type', undefined); - assertEquals('test', variable.name); - assertEquals('test_type', variable.type); - assertNotNull(variable.id_); - variableModelTest_tearDown(); -} - -function testInit_OnlyNameProvided() { - variableModelTest_setUp(); - variable = new Blockly.VariableModel(workspace, 'test'); - assertEquals('test', variable.name); - assertEquals('', variable.type); - assertNotNull(variable.id_); - variableModelTest_tearDown(); -} diff --git a/tests/jsunit/variables_test.js b/tests/jsunit/variables_test.js deleted file mode 100644 index 7041f3174..000000000 --- a/tests/jsunit/variables_test.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @license - * Copyright 2018 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @fileoverview Tests for variable utility functions in Blockly - * @author fenichel@google.com (Rachel Fenichel) - */ -'use strict'; - -function variablesTest_setUp() { - defineGetVarBlock(); - var workspace = new Blockly.Workspace(); - workspace.createVariable('foo', 'type1', '1'); - workspace.createVariable('bar', 'type1', '2'); - workspace.createVariable('baz', 'type1', '3'); - return workspace; -} - -function variablesTest_tearDown(workspace) { - undefineGetVarBlock(); - workspace.dispose(); -} - -function buildVariablesTest(testImpl) { - return function() { - var workspace = variablesTest_setUp(); - try { - testImpl(workspace); - } finally { - variablesTest_tearDown(workspace); - } - }; -} - -var test_allUsedVarModels = buildVariablesTest( - function(workspace) { - createMockVarBlock(workspace, '1'); - createMockVarBlock(workspace, '2'); - createMockVarBlock(workspace, '3'); - - var result = Blockly.Variables.allUsedVarModels(workspace); - assertEquals('Expected three variables in the list of used variables', - 3, result.length); - } -); - -var test_allUsedVarModels_someUnused = buildVariablesTest( - function(workspace) { - createMockVarBlock(workspace, '2'); - - var result = Blockly.Variables.allUsedVarModels(workspace); - assertEquals('Expected one variable in the list of used variables', - 1, result.length); - assertEquals('Expected variable with ID 2 in the list of used variables', - '2', result[0].getId()); - } -); - -var test_allUsedVarModels_varUsedTwice = buildVariablesTest( - function(workspace) { - createMockVarBlock(workspace, '2'); - createMockVarBlock(workspace, '2'); - - var result = Blockly.Variables.allUsedVarModels(workspace); - // Using the same variable multiple times should not change the number of - // elements in the list. - assertEquals('Expected one variable in the list of used variables', - 1, result.length); - assertEquals('Expected variable with ID 2 in the list of used variables', - '2', result[0].getId()); - } -); - -var test_allUsedVarModels_allUnused = buildVariablesTest( - function(workspace) { - var result = Blockly.Variables.allUsedVarModels(workspace); - assertEquals('Expected no variables in the list of used variables', - 0, result.length); - } -); - -var test_getVariable_byId = buildVariablesTest( - function(workspace) { - var var_1 = workspace.createVariable('name1', 'type1', 'id1'); - var var_2 = workspace.createVariable('name2', 'type1', 'id2'); - var var_3 = workspace.createVariable('name3', 'type2', 'id3'); - var result_1 = Blockly.Variables.getVariable(workspace, 'id1'); - var result_2 = Blockly.Variables.getVariable(workspace, 'id2'); - var result_3 = Blockly.Variables.getVariable(workspace, 'id3'); - - assertEquals(var_1, result_1); - assertEquals(var_2, result_2); - assertEquals(var_3, result_3); - } -); - -var test_getVariable_byNameAndType = buildVariablesTest( - function(workspace) { - var var_1 = workspace.createVariable('name1', 'type1', 'id1'); - var var_2 = workspace.createVariable('name2', 'type1', 'id2'); - var var_3 = workspace.createVariable('name3', 'type2', 'id3'); - var result_1 = - Blockly.Variables.getVariable(workspace, null, 'name1', 'type1'); - var result_2 = - Blockly.Variables.getVariable(workspace, null, 'name2', 'type1'); - var result_3 = - Blockly.Variables.getVariable(workspace, null, 'name3', 'type2'); - - // Searching by name + type is correct. - assertEquals(var_1, result_1); - assertEquals(var_2, result_2); - assertEquals(var_3, result_3); - } -); - -var test_getVariable_byIdThenName = buildVariablesTest( - function(workspace) { - var var_1 = workspace.createVariable('name1', 'type1', 'id1'); - var var_2 = workspace.createVariable('name2', 'type1', 'id2'); - var var_3 = workspace.createVariable('name3', 'type2', 'id3'); - var result_1 = - Blockly.Variables.getVariable(workspace, 'badId', 'name1', 'type1'); - var result_2 = - Blockly.Variables.getVariable(workspace, 'badId', 'name2', 'type1'); - var result_3 = - Blockly.Variables.getVariable(workspace, 'badId', 'name3', 'type2'); - - // Searching by ID failed, but falling back onto name + type is correct. - assertEquals(var_1, result_1); - assertEquals(var_2, result_2); - assertEquals(var_3, result_3); - } -); diff --git a/tests/jsunit/widget_div_test.js b/tests/jsunit/widget_div_test.js deleted file mode 100644 index db15dbe37..000000000 --- a/tests/jsunit/widget_div_test.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - - -function widgetdiv_testHelper_makeBBox(left, top, width, height) { - return { - left: left, - right: left + width, - top: top, - bottom: top + height - }; -} - -function widgetdiv_testHelper_makeSize(width, height) { - return { - width: width, - height: height - }; -} - -var widgetDiv_test_viewport = widgetdiv_testHelper_makeBBox(0, 0, 1000, 1000); -var widgetDiv_test_widgetSize = widgetdiv_testHelper_makeSize(100, 100); - -// Anchor is always 90 px wide and 90 px tall for this test. -var widgetDiv_test_anchorSize = 90; - -function widgetdiv_testHelper_makeAnchor(left, top) { - return { - left: left, - right: left + widgetDiv_test_anchorSize, - top: top, - bottom: top + widgetDiv_test_anchorSize - }; -} - -function test_widgetDiv_topConflict() { - var anchorTop = 50; - // Anchor placed close to the top. - var anchorBBox = widgetdiv_testHelper_makeAnchor(500, anchorTop); - - // The widget div should be placed just below the anchor. - var calculated = Blockly.WidgetDiv.calculateY_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize); - assertEquals(anchorTop + widgetDiv_test_anchorSize, calculated); -} - -function test_widgetDiv_bottomConflict() { - var anchorTop = 900; - // Anchor placed close to the bottom. - var anchorBBox = widgetdiv_testHelper_makeAnchor(500, anchorTop); - - // The widget div should be placed just above the anchor. - var calculated = Blockly.WidgetDiv.calculateY_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize); - assertEquals(anchorTop - widgetDiv_test_widgetSize.height, calculated); -} - -function test_widgetDiv_noYConflict() { - var anchorTop = 500; - // Anchor placed in the middle. - var anchorBBox = widgetdiv_testHelper_makeAnchor(500, anchorTop); - - // The widget div should be placed just below the anchor. - var calculated = Blockly.WidgetDiv.calculateY_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize); - assertEquals(anchorTop + widgetDiv_test_anchorSize, calculated); -} - - -function test_widgetDiv_leftConflict_LTR() { - var anchorLeft = 50; - // Anchor placed close to the left side. - var anchorBBox = widgetdiv_testHelper_makeAnchor(anchorLeft, 500); - - // The widget div should be placed at the anchor. - var calculated = Blockly.WidgetDiv.calculateX_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize, false /* rtl */); - assertEquals(anchorLeft, calculated); -} - -function test_widgetDiv_rightConflict_LTR() { - var anchorLeft = 950; - // Anchor placed close to the right side. - var anchorBBox = widgetdiv_testHelper_makeAnchor(anchorLeft, 500); - - // The widget div should be placed as far right as possible--at the edge of - // the screen. - var calculated = Blockly.WidgetDiv.calculateX_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize, false /* rtl */); - assertEquals(1000 - widgetDiv_test_widgetSize.width, calculated); -} - -function test_widgetDiv_noXConflict_LTR() { - var anchorLeft = 500; - // Anchor in the middle - var anchorBBox = widgetdiv_testHelper_makeAnchor(anchorLeft, 500); - // The widget div should be placed just at the left side of the anchor. - var calculated = Blockly.WidgetDiv.calculateX_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize, false /* rtl */); - assertEquals(anchorLeft, calculated); -} - -function test_widgetDiv_leftConflict_RTL() { - var anchorLeft = 10; - // Anchor placed close to the left side. - var anchorBBox = widgetdiv_testHelper_makeAnchor(anchorLeft, 500); - // The widget div should be placed as far left as possible--at the edge of - // the screen. - var calculated = Blockly.WidgetDiv.calculateX_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize, true /* rtl */); - assertEquals(0, calculated); -} - -function test_widgetDiv_rightConflict_RTL() { - var anchorLeft = 950; - // Anchor placed close to the right side. - var anchorBBox = widgetdiv_testHelper_makeAnchor(anchorLeft, 500); - - // The widget div should be placed as far right as possible--at the edge of - // the screen. - var calculated = Blockly.WidgetDiv.calculateX_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize, true /* rtl */); - assertEquals(1000 - widgetDiv_test_widgetSize.width, calculated); -} - -function test_widgetDiv_noXConflict_RTL() { - var anchorLeft = 500; - // anchor placed in the middle - var anchorBBox = widgetdiv_testHelper_makeAnchor(anchorLeft, 500); - // The widget div should be placed at the right side of the anchor. - var calculated = Blockly.WidgetDiv.calculateX_(widgetDiv_test_viewport, - anchorBBox, widgetDiv_test_widgetSize, true /* rtl */); - assertEquals(anchorBBox.right - widgetDiv_test_widgetSize.width, calculated); -} diff --git a/tests/jsunit/workspace_comment_test.js b/tests/jsunit/workspace_comment_test.js deleted file mode 100644 index 9bb630252..000000000 --- a/tests/jsunit/workspace_comment_test.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -var workspace; - -function workspaceCommentTest_setUp() { - workspace = new Blockly.Workspace(); -} - -function workspaceCommentTest_tearDown() { - workspace.dispose(); -} - -function test_noWorkspaceComments() { - workspaceCommentTest_setUp(); - try { - assertEquals('Empty workspace: no comments (1).', 0, workspace.getTopComments(true).length); - assertEquals('Empty workspace: no comments (2).', 0, workspace.getTopComments(false).length); - workspace.clear(); - assertEquals('Empty workspace: no comments (3).', 0, workspace.getTopComments(true).length); - assertEquals('Empty workspace: no comments (4).', 0, workspace.getTopComments(false).length); - } finally { - workspaceCommentTest_tearDown(); - } -} - -function test_oneWorkspaceComment() { - workspaceCommentTest_setUp(); - try { - var comment = new Blockly.WorkspaceComment(workspace, 'comment text', 0, 0, 'comment id'); - assertEquals('One comment on workspace (1).', 1, workspace.getTopComments(true).length); - assertEquals('One comment on workspace (2).', 1, workspace.getTopComments(false).length); - assertEquals('Comment db contains this comment.', comment, workspace.commentDB_['comment id']); - workspace.clear(); - assertEquals('Cleared workspace: no comments (3).', 0, workspace.getTopComments(true).length); - assertEquals('Cleared workspace: no comments (4).', 0, workspace.getTopComments(false).length); - assertFalse('Comment DB does not contain this comment.', 'comment id' in workspace.commentDB_); - } finally { - workspaceCommentTest_tearDown(); - } -} - -function test_getWorkspaceCommentById() { - workspaceCommentTest_setUp(); - try { - var comment = new Blockly.WorkspaceComment(workspace, 'comment text', 0, 0, 'comment id'); - assertEquals('Getting a comment by id.', comment, workspace.getCommentById('comment id')); - assertEquals('No comment found.', null, workspace.getCommentById('not a comment')); - comment.dispose(); - assertEquals('Can\'t find the comment.', null, workspace.getCommentById('comment id')); - } finally { - workspaceCommentTest_tearDown(); - } -} - -function test_disposeWsCommentTwice() { - workspaceCommentTest_setUp(); - try { - var comment = new Blockly.WorkspaceComment(workspace, 'comment text', 0, 0, 'comment id'); - comment.dispose(); - // Nothing should go wrong the second time dispose is called. - comment.dispose(); - } finally { - workspaceCommentTest_tearDown(); - } -} - -function test_wsCommentHeightWidth() { - workspaceCommentTest_setUp(); - try { - var comment = - new Blockly.WorkspaceComment(workspace, 'comment text', 10, 20, 'comment id'); - assertEquals('Initial width', 20, comment.getWidth()); - assertEquals('Initial height', 10, comment.getHeight()); - - comment.setWidth(30); - assertEquals('New width should be different', 30, comment.getWidth()); - assertEquals('New height should not be different', 10, comment.getHeight()); - - comment.setHeight(40); - assertEquals('New width should not be different', 30, comment.getWidth()); - assertEquals('New height should be different', 40, comment.getHeight()); - comment.dispose(); - } finally { - workspaceCommentTest_tearDown(); - } -} - -function test_wsCommentXY() { - workspaceCommentTest_setUp(); - try { - var comment = - new Blockly.WorkspaceComment(workspace, 'comment text', 10, 20, 'comment id'); - var xy = comment.getXY(); - assertEquals('Initial X position', 0, xy.x); - assertEquals('Initial Y position', 0, xy.y); - - comment.moveBy(10, 100); - xy = comment.getXY(); - assertEquals('New X position', 10, xy.x); - assertEquals('New Y position', 100, xy.y); - comment.dispose(); - } finally { - workspaceCommentTest_tearDown(); - } -} - -function test_wsCommentContent() { - workspaceCommentTest_setUp(); - - Blockly.Events.fire = temporary_fireEvent; - temporary_fireEvent.firedEvents_ = []; - try { - var comment = - new Blockly.WorkspaceComment(workspace, 'comment text', 10, 20, 'comment id'); - assertEquals( - 'Check comment text', 'comment text', comment.getContent()); - assertEquals( - 'Workspace undo stack has one event', 1, workspace.undoStack_.length); - - comment.setContent('comment text'); - assertEquals( - 'Comment text has not changed', 'comment text', comment.getContent()); - // Setting the text to the old value does not fire an event. - assertEquals( - 'Workspace undo stack has one event', 1, workspace.undoStack_.length); - - comment.setContent('new comment text'); - assertEquals( - 'Comment text has changed', 'new comment text', comment.getContent()); - assertEquals( - 'Workspace undo stack has two events', 2, workspace.undoStack_.length); - comment.dispose(); - } finally { - workspaceCommentTest_tearDown(); - Blockly.Events.fire = savedFireFunc; - } -} diff --git a/tests/jsunit/workspace_test.js b/tests/jsunit/workspace_test.js deleted file mode 100644 index 6576f0346..000000000 --- a/tests/jsunit/workspace_test.js +++ /dev/null @@ -1,361 +0,0 @@ -/** - * @license - * Copyright 2012 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - - -var workspace; -var mockControl_; - -function workspaceTest_setUp() { - defineGetVarBlock(); - workspace = new Blockly.Workspace(); -} - -function workspaceTest_tearDown() { - undefineGetVarBlock(); - if (mockControl_) { - mockControl_.restore(); - } - workspace.dispose(); -} - -function test_emptyWorkspace() { - workspaceTest_setUp(); - try { - assertEquals('Empty workspace (1).', 0, workspace.getTopBlocks(true).length); - assertEquals('Empty workspace (2).', 0, workspace.getTopBlocks(false).length); - assertEquals('Empty workspace (3).', 0, workspace.getAllBlocks(false).length); - workspace.clear(); - assertEquals('Empty workspace (4).', 0, workspace.getTopBlocks(true).length); - assertEquals('Empty workspace (5).', 0, workspace.getTopBlocks(false).length); - assertEquals('Empty workspace (6).', 0, workspace.getAllBlocks(false).length); - } finally { - workspaceTest_tearDown(); - } -} - -function test_flatWorkspace() { - workspaceTest_setUp(); - try { - var blockA = workspace.newBlock(''); - assertEquals('One block workspace (1).', 1, workspace.getTopBlocks(true).length); - assertEquals('One block workspace (2).', 1, workspace.getTopBlocks(false).length); - assertEquals('One block workspace (3).', 1, workspace.getAllBlocks(false).length); - var blockB = workspace.newBlock(''); - assertEquals('Two block workspace (1).', 2, workspace.getTopBlocks(true).length); - assertEquals('Two block workspace (2).', 2, workspace.getTopBlocks(false).length); - assertEquals('Two block workspace (3).', 2, workspace.getAllBlocks(false).length); - blockA.dispose(); - assertEquals('One block workspace (4).', 1, workspace.getTopBlocks(true).length); - assertEquals('One block workspace (5).', 1, workspace.getTopBlocks(false).length); - assertEquals('One block workspace (6).', 1, workspace.getAllBlocks(false).length); - workspace.clear(); - assertEquals('Cleared workspace (1).', 0, workspace.getTopBlocks(true).length); - assertEquals('Cleared workspace (2).', 0, workspace.getTopBlocks(false).length); - assertEquals('Cleared workspace (3).', 0, workspace.getAllBlocks(false).length); - } finally { - workspaceTest_tearDown(); - } -} - -function test_maxBlocksWorkspace() { - workspaceTest_setUp(); - try { - var blockA = workspace.newBlock(''); - var blockB = workspace.newBlock(''); - assertEquals('Infinite capacity.', Infinity, workspace.remainingCapacity()); - workspace.options.maxBlocks = 3; - assertEquals('Three capacity.', 1, workspace.remainingCapacity()); - workspace.options.maxBlocks = 2; - assertEquals('Two capacity.', 0, workspace.remainingCapacity()); - workspace.options.maxBlocks = 1; - assertEquals('One capacity.', -1, workspace.remainingCapacity()); - workspace.options.maxBlocks = 0; - assertEquals('Zero capacity.', -2, workspace.remainingCapacity()); - workspace.clear(); - assertEquals('Cleared capacity.', 0, workspace.remainingCapacity()); - } finally { - workspaceTest_tearDown(); - } -} - -function test_getWorkspaceById() { - var workspaceA = new Blockly.Workspace(); - var workspaceB = new Blockly.Workspace(); - try { - assertEquals('Find workspaceA.', workspaceA, - Blockly.Workspace.getById(workspaceA.id)); - assertEquals('Find workspaceB.', workspaceB, - Blockly.Workspace.getById(workspaceB.id)); - assertEquals('No workspace found.', null, - Blockly.Workspace.getById('I do not exist.')); - workspaceA.dispose(); - assertEquals('Can\'t find workspaceA.', null, - Blockly.Workspace.getById(workspaceA.id)); - assertEquals('WorkspaceB exists.', workspaceB, - Blockly.Workspace.getById(workspaceB.id)); - } finally { - workspaceB.dispose(); - workspaceA.dispose(); - } -} - -function test_getBlockById() { - workspaceTest_setUp(); - try { - var blockA = workspace.newBlock(''); - var blockB = workspace.newBlock(''); - assertEquals('Find blockA.', blockA, workspace.getBlockById(blockA.id)); - assertEquals('Find blockB.', blockB, workspace.getBlockById(blockB.id)); - assertEquals('No block found.', null, - workspace.getBlockById('I do not exist.')); - blockA.dispose(); - assertEquals('Can\'t find blockA.', null, workspace.getBlockById(blockA.id)); - assertEquals('BlockB exists.', blockB, workspace.getBlockById(blockB.id)); - workspace.clear(); - assertEquals('Can\'t find blockB.', null, workspace.getBlockById(blockB.id)); - } finally { - workspaceTest_tearDown(); - } -} - -function test_deleteVariable_InternalTrivial() { - workspaceTest_setUp(); - var var_1 = workspace.createVariable('name1', 'type1', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); - createMockVarBlock(workspace, 'id1'); - createMockVarBlock(workspace, 'id1'); - createMockVarBlock(workspace, 'id2'); - - var uses = workspace.getVariableUsesById(var_1.getId()); - workspace.deleteVariableInternal_(var_1, uses); - - var variable = workspace.getVariableById('id1'); - var block_var_name = workspace.topBlocks_[0].getVarModels()[0].name; - assertNull(variable); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - assertEquals('name2', block_var_name); - workspaceTest_tearDown(); -} - -// TODO(marisaleung): Test the alert for deleting a variable that is a procedure. - -function test_addTopBlock_TrivialFlyoutIsTrue() { - workspaceTest_setUp(); - var targetWorkspace = new Blockly.Workspace(); - workspace.isFlyout = true; - workspace.targetWorkspace = targetWorkspace; - targetWorkspace.createVariable('name1', '', '1'); - - // Flyout.init usually does this binding. - workspace.variableMap_ = targetWorkspace.getVariableMap(); - - try { - var block = createMockVarBlock(workspace, '1'); - workspace.removeTopBlock(block); - workspace.addTopBlock(block); - checkVariableValues(workspace, 'name1', '', '1'); - } finally { - workspaceTest_tearDown(); - // Have to dispose of the main workspace after the flyout workspace, because - // it holds the variable map. - // Normally the main workspace disposes of the flyout workspace. - targetWorkspace.dispose(); - } -} - -function test_clear_Trivial() { - workspaceTest_setUp(); - workspace.createVariable('name1', 'type1', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); - mockControl_ = setUpMockMethod(Blockly.Events, 'setGroup', [true, false], - null); - - try { - workspace.clear(); - var topBlocks_length = workspace.topBlocks_.length; - var varMapLength = Object.keys(workspace.variableMap_.variableMap_).length; - assertEquals(0, topBlocks_length); - assertEquals(0, varMapLength); - } finally { - workspaceTest_tearDown(); - } -} - -function test_clear_NoVariables() { - workspaceTest_setUp(); - mockControl_ = setUpMockMethod(Blockly.Events, 'setGroup', [true, false], - null); - - try { - workspace.clear(); - var topBlocks_length = workspace.topBlocks_.length; - var varMapLength = Object.keys(workspace.variableMap_.variableMap_).length; - assertEquals(0, topBlocks_length); - assertEquals(0, varMapLength); - } finally { - workspaceTest_tearDown(); - } -} - -function test_renameVariable_NoReference() { - // Test renaming a variable in the simplest case: when no blocks refer to it. - workspaceTest_setUp(); - var id = 'id1'; - var type = 'type1'; - var oldName = 'name1'; - var newName = 'name2'; - workspace.createVariable(oldName, type, id); - - try { - workspace.renameVariableById(id, newName); - checkVariableValues(workspace, newName, type, id); - // Renaming should not have created a new variable. - assertEquals(1, workspace.getAllVariables().length); - } finally { - workspaceTest_tearDown(); - } -} - -function test_renameVariable_ReferenceExists() { - // Test renaming a variable when a reference to it exists. - // Expect 'renameVariable' to change oldName variable name to newName. - workspaceTest_setUp(); - var newName = 'name2'; - - createVariableAndBlock(workspace); - - workspace.renameVariableById('id1', newName); - checkVariableValues(workspace, newName, 'type1', 'id1'); - // Renaming should not have created a new variable. - assertEquals(1, workspace.getAllVariables().length); - var block_var_name = workspace.topBlocks_[0].getVarModels()[0].name; - assertEquals(newName, block_var_name); - workspaceTest_tearDown(); -} - -function test_renameVariable_TwoVariablesSameType() { - // Expect 'renameVariable' to change oldName variable name to newName. - // Expect oldName block name to change to newName - workspaceTest_setUp(); - var id1 = 'id1'; - var id2 = 'id2'; - var type = 'type1'; - - var oldName = 'name1'; - var newName = 'name2'; - // Create two variables of the same type. - workspace.createVariable(oldName, type, id1); - workspace.createVariable(newName, type, id2); - // Create blocks to refer to both of them. - createMockVarBlock(workspace, id1); - createMockVarBlock(workspace, id2); - - workspace.renameVariableById(id1, newName); - checkVariableValues(workspace, newName, type, id2); - // The old variable should have been deleted. - var variable = workspace.getVariableById(id1); - assertNull(variable); - - // There should only be one variable left. - assertEquals(1, workspace.getAllVariables().length); - - // References should have the correct names. - var block_var_name_1 = workspace.topBlocks_[0].getVarModels()[0].name; - var block_var_name_2 = workspace.topBlocks_[1].getVarModels()[0].name; - assertEquals(newName, block_var_name_1); - assertEquals(newName, block_var_name_2); - - workspaceTest_tearDown(); -} - -function test_renameVariable_TwoVariablesDifferentType() { - // Expect the rename to succeed, because variables with different types are - // allowed to have the same name. - workspaceTest_setUp(); - createTwoVariablesAndBlocks(workspace); - - var newName = 'name2'; - workspace.renameVariableById('id1', newName); - - checkVariableValues(workspace, newName, 'type1', 'id1'); - checkVariableValues(workspace, newName, 'type2', 'id2'); - - // References shoul have the correct names. - var block_var_name_1 = workspace.topBlocks_[0].getVarModels()[0].name; - var block_var_name_2 = workspace.topBlocks_[1].getVarModels()[0].name; - assertEquals(newName, block_var_name_1); - assertEquals(newName, block_var_name_2); - - workspaceTest_tearDown(); -} - -function test_renameVariable_OldCase() { - // Rename a variable with a single reference. Update only the capitalization. - workspaceTest_setUp(); - var newName = 'Name1'; - - createVariableAndBlock(workspace); - - workspace.renameVariableById('id1', newName); - checkVariableValues(workspace, newName, 'type1', 'id1'); - var variable = workspace.getVariableById('id1'); - assertNotEquals('name1', variable.name); - workspaceTest_tearDown(); -} - -function test_renameVariable_TwoVariablesAndOldCase() { - // Test renaming a variable to an in-use name, but with different - // capitalization. The new capitalization should apply everywhere. - - // TODO (fenichel): What about different capitalization but also different - // types? - workspaceTest_setUp(); - var oldName = 'name1'; - var oldCase = 'Name2'; - var newName = 'name2'; - - var id1 = 'id1'; - var id2 = 'id2'; - - var type = 'type1'; - - workspace.createVariable(oldName, type, id1); - workspace.createVariable(oldCase, type, id2); - createMockVarBlock(workspace, id1); - createMockVarBlock(workspace, id2); - - workspace.renameVariableById(id1, newName); - - checkVariableValues(workspace, newName, type, id2); - - // The old variable should have been deleted. - var variable = workspace.getVariableById(id1); - assertNull(variable); - - // There should only be one variable left. - assertEquals(1, workspace.getAllVariables().length); - - // Blocks should now use the new capitalization. - var block_var_name_1 = workspace.topBlocks_[0].getVarModels()[0].name; - var block_var_name_2 = workspace.topBlocks_[1].getVarModels()[0].name; - assertEquals(newName, block_var_name_1); - assertEquals(newName, block_var_name_2); - workspaceTest_tearDown(); -} - -function test_deleteVariableById_Trivial() { - workspaceTest_setUp(); - createTwoVariablesAndBlocks(workspace); - - workspace.deleteVariableById('id1'); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - var variable = workspace.getVariableById('id1'); - var block_var_name = workspace.topBlocks_[0].getVarModels()[0].name; - assertNull(variable); - assertEquals('name2', block_var_name); - workspaceTest_tearDown(); -} diff --git a/tests/jsunit/workspace_undo_redo_test.js b/tests/jsunit/workspace_undo_redo_test.js deleted file mode 100644 index d9745d700..000000000 --- a/tests/jsunit/workspace_undo_redo_test.js +++ /dev/null @@ -1,363 +0,0 @@ -/** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - - /** - * @fileoverview Tests for Blockly.Workspace.undo. - * @author marisaleung@google.com (Marisa Leung) - */ -'use strict'; - - -var workspace; -var mockControl_; -var savedFireFunc = Blockly.Events.fire; - -function temporary_fireEvent(event) { - if (!Blockly.Events.isEnabled()) { - return; - } - Blockly.Events.FIRE_QUEUE_.push(event); - Blockly.Events.fireNow_(); -} - -function undoRedoTest_setUp() { - defineGetVarBlock(); - workspace = new Blockly.Workspace(); - Blockly.Events.fire = temporary_fireEvent; -} - -function undoRedoTest_tearDown() { - undefineGetVarBlock(); - if (mockControl_) { - mockControl_.restore(); - } - workspace.dispose(); - Blockly.Events.fire = savedFireFunc; -} - -/** - * Check that the top block with the given index contains a variable with - * the given name. - * @param {number} blockIndex The index of the top block. - * @param {string} name The expected name of the variable in the block. - */ -function undoRedoTest_checkBlockVariableName(blockIndex, name) { - var blockVarName = workspace.topBlocks_[blockIndex].getVarModels()[0].name; - assertEquals(name, blockVarName); -} - -function createTwoVarsEmptyType() { - workspace.createVariable('name1', '', 'id1'); - workspace.createVariable('name2', '', 'id2'); -} - -function createTwoVarsDifferentTypes() { - workspace.createVariable('name1', 'type1', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); -} - -function test_undoCreateVariable_Trivial() { - undoRedoTest_setUp(); - createTwoVarsDifferentTypes(); - - workspace.undo(); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - assertNull(workspace.getVariableById('id2')); - workspace.undo(); - assertNull(workspace.getVariableById('id1')); - assertNull(workspace.getVariableById('id2')); - undoRedoTest_tearDown(); -} - -function test_redoAndUndoCreateVariable_Trivial() { - undoRedoTest_setUp(); - createTwoVarsDifferentTypes(); - - workspace.undo(); - workspace.undo(true); - - // Expect that variable 'id2' is recreated - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - - workspace.undo(); - workspace.undo(); - workspace.undo(true); - - // Expect that variable 'id1' is recreated - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - assertNull(workspace.getVariableById('id2')); - undoRedoTest_tearDown(); -} - -function test_undoDeleteVariable_NoBlocks() { - undoRedoTest_setUp(); - createTwoVarsDifferentTypes(); - workspace.deleteVariableById('id1'); - workspace.deleteVariableById('id2'); - - workspace.undo(); - assertNull(workspace.getVariableById('id1')); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - - workspace.undo(); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - undoRedoTest_tearDown(); -} - -function test_undoDeleteVariable_WithBlocks() { - undoRedoTest_setUp(); - - createTwoVariablesAndBlocks(workspace); - - workspace.deleteVariableById('id1'); - workspace.deleteVariableById('id2'); - - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name2'); - assertNull(workspace.getVariableById('id1')); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name2'); - undoRedoTest_checkBlockVariableName(1, 'name1'); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - undoRedoTest_tearDown(); -} - -function test_redoAndUndoDeleteVariable_NoBlocks() { - undoRedoTest_setUp(); - - createTwoVarsDifferentTypes(); - - workspace.deleteVariableById('id1'); - workspace.deleteVariableById('id2'); - - workspace.undo(); - workspace.undo(true); - // Expect that both variables are deleted - assertNull(workspace.getVariableById('id1')); - assertNull(workspace.getVariableById('id2')); - - workspace.undo(); - workspace.undo(); - workspace.undo(true); - // Expect that variable 'id2' is recreated - assertNull(workspace.getVariableById('id1')); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - undoRedoTest_tearDown(); -} - -function test_redoAndUndoDeleteVariable_WithBlocks() { - undoRedoTest_setUp(); - - createTwoVariablesAndBlocks(workspace); - - workspace.deleteVariableById('id1'); - workspace.deleteVariableById('id2'); - - workspace.undo(); - workspace.undo(true); - // Expect that both variables are deleted - assertEquals(0, workspace.topBlocks_.length); - assertNull(workspace.getVariableById('id1')); - assertNull(workspace.getVariableById('id2')); - - workspace.undo(); - workspace.undo(); - workspace.undo(true); - // Expect that variable 'id2' is recreated - undoRedoTest_checkBlockVariableName(0, 'name2'); - assertNull(workspace.getVariableById('id1')); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - undoRedoTest_tearDown(); -} - -function test_redoAndUndoDeleteVariableTwice_NoBlocks() { - undoRedoTest_setUp(); - workspace.createVariable('name1', 'type1', 'id1'); - workspace.deleteVariableById('id1'); - workspace.deleteVariableById('id1'); - - // Check the undoStack only recorded one delete event. - var undoStack = workspace.undoStack_; - assertEquals('var_delete', undoStack[undoStack.length-1].type); - assertNotEquals('var_delete', undoStack[undoStack.length-2].type); - - // undo delete - workspace.undo(); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - - // redo delete - workspace.undo(true); - assertNull(workspace.getVariableById('id1')); - - // redo delete, nothing should happen - workspace.undo(true); - assertNull(workspace.getVariableById('id1')); - undoRedoTest_tearDown(); -} - -function test_redoAndUndoDeleteVariableTwice_WithBlocks() { - undoRedoTest_setUp(); - var id = 'id1'; - workspace.createVariable('name1', 'type1', id); - createMockVarBlock(workspace, id); - workspace.deleteVariableById(id); - workspace.deleteVariableById(id); - - // Check the undoStack only recorded one delete event. - var undoStack = workspace.undoStack_; - assertEquals('var_delete', undoStack[undoStack.length-1].type); - assertEquals('delete', undoStack[undoStack.length-2].type); - assertNotEquals('var_delete', undoStack[undoStack.length-3].type); - - // undo delete - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name1'); - checkVariableValues(workspace, 'name1', 'type1', id); - - // redo delete - workspace.undo(true); - assertEquals(0, workspace.topBlocks_.length); - assertNull(workspace.getVariableById(id)); - - // redo delete, nothing should happen - workspace.undo(true); - assertEquals(0, workspace.topBlocks_.length); - assertNull(workspace.getVariableById(id)); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_OneExists_NoBlocks() { - undoRedoTest_setUp(); - workspace.createVariable('name1', '', 'id1'); - workspace.renameVariableById('id1', 'name2'); - - workspace.undo(); - checkVariableValues(workspace, 'name1', '', 'id1'); - - workspace.undo(true); - checkVariableValues(workspace, 'name2', '', 'id1'); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_OneExists_WithBlocks() { - undoRedoTest_setUp(); - workspace.createVariable('name1', '', 'id1'); - createMockVarBlock(workspace, 'id1'); - workspace.renameVariableById('id1', 'name2'); - - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name1'); - checkVariableValues(workspace, 'name1', '', 'id1'); - - workspace.undo(true); - checkVariableValues(workspace, 'name2', '', 'id1'); - undoRedoTest_checkBlockVariableName(0, 'name2'); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_BothExist_NoBlocks() { - undoRedoTest_setUp(); - createTwoVarsEmptyType(); - workspace.renameVariableById('id1', 'name2'); - - workspace.undo(); - checkVariableValues(workspace, 'name1', '', 'id1'); - checkVariableValues(workspace, 'name2', '', 'id2'); - - workspace.undo(true); - checkVariableValues(workspace, 'name2', '', 'id2'); - assertNull(workspace.getVariableById('id1')); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_BothExist_WithBlocks() { - undoRedoTest_setUp(); - createTwoVarsEmptyType(); - createMockVarBlock(workspace, 'id1'); - createMockVarBlock(workspace, 'id2'); - workspace.renameVariableById('id1', 'name2'); - - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name1'); - undoRedoTest_checkBlockVariableName(1, 'name2'); - checkVariableValues(workspace, 'name1', '', 'id1'); - checkVariableValues(workspace, 'name2', '', 'id2'); - - workspace.undo(true); - undoRedoTest_checkBlockVariableName(0, 'name2'); - undoRedoTest_checkBlockVariableName(1, 'name2'); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_BothExistCaseChange_NoBlocks() { - undoRedoTest_setUp(); - createTwoVarsEmptyType(); - workspace.renameVariableById('id1', 'Name2'); - - workspace.undo(); - checkVariableValues(workspace, 'name1', '', 'id1'); - checkVariableValues(workspace, 'name2', '', 'id2'); - - workspace.undo(true); - checkVariableValues(workspace, 'Name2', '', 'id2'); - assertNull(workspace.getVariable('name1')); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_BothExistCaseChange_WithBlocks() { - undoRedoTest_setUp(); - createTwoVarsEmptyType(); - createMockVarBlock(workspace, 'id1'); - createMockVarBlock(workspace, 'id2'); - workspace.renameVariableById('id1', 'Name2'); - - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name1'); - undoRedoTest_checkBlockVariableName(1, 'name2'); - checkVariableValues(workspace, 'name1', '', 'id1'); - checkVariableValues(workspace, 'name2', '', 'id2'); - - workspace.undo(true); - checkVariableValues(workspace, 'Name2', '', 'id2'); - assertNull(workspace.getVariableById('id1')); - undoRedoTest_checkBlockVariableName(0, 'Name2'); - undoRedoTest_checkBlockVariableName(1, 'Name2'); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_OnlyCaseChange_NoBlocks() { - undoRedoTest_setUp(); - workspace.createVariable('name1', '', 'id1'); - workspace.renameVariableById('id1', 'Name1'); - - workspace.undo(); - checkVariableValues(workspace, 'name1', '', 'id1'); - - workspace.undo(true); - checkVariableValues(workspace, 'Name1', '', 'id1'); - undoRedoTest_tearDown(); -} - -function test_undoRedoRenameVariable_OnlyCaseChange_WithBlocks() { - undoRedoTest_setUp(); - workspace.createVariable('name1', '', 'id1'); - createMockVarBlock(workspace, 'id1'); - workspace.renameVariableById('id1', 'Name1'); - - workspace.undo(); - undoRedoTest_checkBlockVariableName(0, 'name1'); - checkVariableValues(workspace, 'name1', '', 'id1'); - - workspace.undo(true); - checkVariableValues(workspace, 'Name1', '', 'id1'); - undoRedoTest_checkBlockVariableName(0, 'Name1'); - undoRedoTest_tearDown(); -} diff --git a/tests/jsunit/xml_test.js b/tests/jsunit/xml_test.js deleted file mode 100644 index 117cd4ed6..000000000 --- a/tests/jsunit/xml_test.js +++ /dev/null @@ -1,375 +0,0 @@ -/** - * @license - * Copyright 2014 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -'use strict'; - -var mockControl_; -var workspace; -var XML_TEXT = ['', - ' ', - ' ', - ' ', - ' 10', - ' ', - ' ', - ' ', - ' ', - ' item', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' Hello', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ''].join('\n'); - -function xmlTest_setUp() { - workspace = new Blockly.Workspace(); -} - -function xmlTest_setUpWithMockBlocks() { - xmlTest_setUp(); - Blockly.defineBlocksWithJsonArray([{ - 'type': 'field_variable_test_block', - 'message0': '%1', - 'args0': [ - { - 'type': 'field_variable', - 'name': 'VAR', - 'variable': 'item' - } - ] - }]); -} - -function xmlTest_tearDown() { - if (mockControl_) { - mockControl_.restore(); - } - workspace.dispose(); -} - -function xmlTest_tearDownWithMockBlocks() { - xmlTest_tearDown(); - delete Blockly.Blocks.field_variable_test_block; -} - -/** - * Check the values of the non variable field DOM. - * @param {!Element} fieldDom The XML DOM of the non variable field. - * @param {!string} name The expected name of the variable. - * @param {!string} text The expected text of the variable. - */ -function xmlTest_checkNonVariableField(fieldDom, name, text) { - assertEquals('textContent', text, fieldDom.textContent); - assertEquals('name', name, fieldDom.getAttribute('name')); - assertNull('id', fieldDom.getAttribute('id')); - assertNull('variabletype', fieldDom.getAttribute('variabletype')); -} - -/** - * Check the values of the variable field DOM. - * @param {!Element} fieldDom The XML DOM of the variable field. - * @param {!string} name The expected name of the variable. - * @param {!string} type The expected type of the variable. - * @param {!string} id The expected ID of the variable. - * @param {!string} text The expected text of the variable. - */ -function xmlTest_checkVariableFieldDomValues(fieldDom, name, type, id, text) { - assertEquals('name', name, fieldDom.getAttribute('name')); - assertEquals('variabletype', type, fieldDom.getAttribute('variabletype')); - assertEquals('id', id, fieldDom.getAttribute('id')); - assertEquals('textContent', text, fieldDom.textContent); -} - -/** - * Check the values of the variable DOM. - * @param {!Element} variableDom The XML DOM of the variable. - * @param {!string} type The expected type of the variable. - * @param {!string} id The expected ID of the variable. - * @param {!string} text The expected text of the variable. - */ -function xmlTest_checkVariableDomValues(variableDom, type, id, text) { - assertEquals('type', type, variableDom.getAttribute('type')); - assertEquals('id', id, variableDom.getAttribute('id')); - assertEquals('textContent', text, variableDom.textContent); -} - -function test_textToDom() { - var dom = Blockly.Xml.textToDom(XML_TEXT); - assertEquals('XML tag', 'xml', dom.nodeName); - assertEquals('Block tags', 6, dom.getElementsByTagName('block').length); -} - -function test_domToText() { - var dom = Blockly.Xml.textToDom(XML_TEXT); - var text = Blockly.Xml.domToText(dom); - assertEquals('Round trip', XML_TEXT.replace(/\s+/g, ''), - text.replace(/\s+/g, '')); -} - -function test_domToWorkspace_BackwardCompatibility() { - // Expect that workspace still loads without serialized variables. - xmlTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '1']); - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' name1' + - ' ' + - ''); - Blockly.Xml.domToWorkspace(dom, workspace); - assertEquals('Block count', 1, workspace.getAllBlocks(false).length); - checkVariableValues(workspace, 'name1', '', '1'); - } finally { - xmlTest_tearDownWithMockBlocks(); - } -} - -function test_domToWorkspace_VariablesAtTop() { - // Expect that unused variables are preserved. - xmlTest_setUpWithMockBlocks(); - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' name1' + - ' name2' + - ' name3' + - ' ' + - ' ' + - ' name3' + - ' ' + - ''); - Blockly.Xml.domToWorkspace(dom, workspace); - assertEquals('Block count', 1, workspace.getAllBlocks(false).length); - checkVariableValues(workspace, 'name1', 'type1', 'id1'); - checkVariableValues(workspace, 'name2', 'type2', 'id2'); - checkVariableValues(workspace, 'name3', '', 'id3'); - } finally { - xmlTest_tearDownWithMockBlocks(); - } -} - -function test_domToWorkspace_VariablesAtTop_DuplicateVariablesTag() { - // Expect thrown Error because of duplicate 'variables' tag. - xmlTest_setUpWithMockBlocks(); - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ''); - Blockly.Xml.domToWorkspace(dom, workspace); - fail(); - } - catch (e) { - // expected - } finally { - xmlTest_tearDownWithMockBlocks(); - } -} - -function test_domToWorkspace_VariablesAtTop_MissingType() { - // Expect thrown error when a variable tag is missing the type attribute. - workspace = new Blockly.Workspace(); - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' name1' + - ' ' + - ' ' + - ' name3' + - ' ' + - ''); - Blockly.Xml.domToWorkspace(dom, workspace); - fail(); - } catch (e) { - // expected - } finally { - workspace.dispose(); - } -} - -function test_domToWorkspace_VariablesAtTop_MismatchBlockType() { - // Expect thrown error when the serialized type of a variable does not match - // the type of a variable field that references it. - xmlTest_setUpWithMockBlocks(); - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' name1' + - ' ' + - ' ' + - ' name1' + - ' ' + - ''); - Blockly.Xml.domToWorkspace(dom, workspace); - fail(); - } catch (e) { - // expected - } finally { - xmlTest_tearDownWithMockBlocks(); - } -} - -function test_domToPrettyText() { - var dom = Blockly.Xml.textToDom(XML_TEXT); - var text = Blockly.Xml.domToPrettyText(dom); - assertEquals('Round trip', XML_TEXT.replace(/\s+/g, ''), - text.replace(/\s+/g, '')); -} - -/** - * Tests the that appendDomToWorkspace works in a headless mode. - * Also see test_appendDomToWorkspace() in workspace_svg_test.js. - */ -function test_appendDomToWorkspace() { - Blockly.Blocks.test_block = { - init: function() { - this.jsonInit({ - message0: 'test', - }); - } - }; - - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ''); - workspace = new Blockly.Workspace(); - Blockly.Xml.appendDomToWorkspace(dom, workspace); - assertEquals('Block count', 1, workspace.getAllBlocks(false).length); - var newBlockIds = Blockly.Xml.appendDomToWorkspace(dom, workspace); - assertEquals('Block count', 2, workspace.getAllBlocks(false).length); - assertEquals('Number of new block ids',1,newBlockIds.length); - } finally { - delete Blockly.Blocks.test_block; - workspace.dispose(); - } -} - -function test_blockToDom_fieldToDom_defaultCase() { - xmlTest_setUpWithMockBlocks(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1', '1']); - try { - workspace.createVariable('name1'); - - Blockly.Events.disable(); - var block = new Blockly.Block(workspace, 'field_variable_test_block'); - block.inputList[0].fieldRow[0].setValue('1'); - Blockly.Events.enable(); - - var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; - // Expect type is null and ID is '1' since we don't specify type and ID. - xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', null, '1', - 'name1'); - } finally { - xmlTest_tearDownWithMockBlocks(); - } -} - -function test_blockToDom_fieldToDom_notAFieldVariable() { - Blockly.defineBlocksWithJsonArray([{ - "type": "field_angle_test_block", - "message0": "%1", - "args0": [ - { - "type": "field_angle", - "name": "VAR", - "angle": 90 - } - ], - }]); - xmlTest_setUpWithMockBlocks(); - var block = new Blockly.Block(workspace, 'field_angle_test_block'); - var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; - xmlTest_checkNonVariableField(resultFieldDom, 'VAR', '90'); - delete Blockly.Blocks.field_angle_block; - xmlTest_tearDownWithMockBlocks(); -} - -function test_variablesToDom_oneVariable() { - xmlTest_setUp(); - mockControl_ = setUpMockMethod(Blockly.utils, 'genUid', null, ['1']); - - workspace.createVariable('name1'); - var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); - assertEquals(1, resultDom.children.length); - var resultVariableDom = resultDom.children[0]; - assertEquals('name1', resultVariableDom.textContent); - assertEquals(null, resultVariableDom.getAttribute('type')); - assertEquals('1', resultVariableDom.getAttribute('id')); - xmlTest_tearDown(); -} - -function test_variablesToDom_twoVariables_oneBlock() { - xmlTest_setUpWithMockBlocks(); - - workspace.createVariable('name1', '', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); - // If events are enabled during block construction, it will create a default - // variable. - Blockly.Events.disable(); - var block = new Blockly.Block(workspace, 'field_variable_test_block'); - block.inputList[0].fieldRow[0].setValue('id1'); - Blockly.Events.enable(); - - var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); - assertEquals(2, resultDom.children.length); - xmlTest_checkVariableDomValues(resultDom.children[0], null, 'id1', - 'name1'); - xmlTest_checkVariableDomValues(resultDom.children[1], 'type2', 'id2', - 'name2'); - xmlTest_tearDownWithMockBlocks(); -} - -function test_variablesToDom_noVariables() { - xmlTest_setUp(); - workspace.createVariable('name1'); - var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); - assertEquals(1, resultDom.children.length); - xmlTest_tearDown(); -} - -function test_variableFieldXml_caseSensitive() { - var id = 'testId'; - var type = 'testType'; - var name = 'testName'; - - var mockVariableModel = { - type: type, - name: name, - getId: function() { - return id; - } - }; - - var generatedXml = Blockly.Xml.domToText( - Blockly.Variables.generateVariableFieldDom(mockVariableModel)); - var goldenXml = - '' + name + ''; - assertEquals(goldenXml, generatedXml); -} diff --git a/tests/mocha/.eslintrc.json b/tests/mocha/.eslintrc.json index b4bb40fec..705a79397 100644 --- a/tests/mocha/.eslintrc.json +++ b/tests/mocha/.eslintrc.json @@ -6,18 +6,19 @@ "globals": { "chai": false, "sinon": false, - "assertNull": true, - "assertNotNull": true, - "assertEquals": true, - "assertTrue": true, - "assertFalse": true, - "isEqualArrays": true, - "assertUndefined": true, - "assertNotUndefined": true, - "defineStackBlock": true, + "assertArrayEquals": true, + "assertVariableValues": true, + "captureWarnings": true, + "createTestBlock": true, "defineRowBlock": true, - "defineStatementBlock": true - + "defineStackBlock": true, + "defineStatementBlock": true, + "createEventsFireStub": true, + "testAWorkspace": true, + "testHelpers" : true, + "getSimpleJSON": true, + "getXmlArray": true, + "getCategoryJSON": true }, "extends": "../../.eslintrc.json" } diff --git a/tests/mocha/astnode_test.js b/tests/mocha/astnode_test.js index 2d0b111a7..793bdcf9a 100644 --- a/tests/mocha/astnode_test.js +++ b/tests/mocha/astnode_test.js @@ -105,7 +105,7 @@ suite('ASTNode', function() { var connection = input.connection; var node = Blockly.ASTNode.createConnectionNode(connection); var newASTNode = node.findNextForInput_(input); - assertEquals(newASTNode.getLocation(), input2.connection); + chai.assert.equal(newASTNode.getLocation(), input2.connection); }); test('findPrevForInput_', function() { @@ -114,7 +114,7 @@ suite('ASTNode', function() { var connection = input2.connection; var node = Blockly.ASTNode.createConnectionNode(connection); var newASTNode = node.findPrevForInput_(input2); - assertEquals(newASTNode.getLocation(), input.connection); + chai.assert.equal(newASTNode.getLocation(), input.connection); }); test('findNextForField_', function() { @@ -122,7 +122,7 @@ suite('ASTNode', function() { var field2 = this.blocks.statementInput1.inputList[0].fieldRow[1]; var node = Blockly.ASTNode.createFieldNode(field); var newASTNode = node.findNextForField_(field); - assertEquals(newASTNode.getLocation(), field2); + chai.assert.equal(newASTNode.getLocation(), field2); }); test('findPrevForField_', function() { @@ -130,469 +130,415 @@ suite('ASTNode', function() { var field2 = this.blocks.statementInput1.inputList[0].fieldRow[1]; var node = Blockly.ASTNode.createFieldNode(field2); var newASTNode = node.findPrevForField_(field2); - assertEquals(newASTNode.getLocation(), field); + chai.assert.equal(newASTNode.getLocation(), field); }); test('navigateBetweenStacks_Forward', function() { var node = new Blockly.ASTNode( Blockly.ASTNode.types.NEXT, this.blocks.statementInput1.nextConnection); var newASTNode = node.navigateBetweenStacks_(true); - assertEquals(newASTNode.getLocation(), this.blocks.statementInput4); + chai.assert.equal(newASTNode.getLocation(), this.blocks.statementInput4); }); test('navigateBetweenStacks_Backward', function() { var node = new Blockly.ASTNode( Blockly.ASTNode.types.BLOCK, this.blocks.statementInput4); var newASTNode = node.navigateBetweenStacks_(false); - assertEquals(newASTNode.getLocation(), this.blocks.statementInput1); + chai.assert.equal(newASTNode.getLocation(), this.blocks.statementInput1); }); test('getOutAstNodeForBlock_', function() { var node = new Blockly.ASTNode( Blockly.ASTNode.types.BLOCK, this.blocks.statementInput2); var newASTNode = node.getOutAstNodeForBlock_(this.blocks.statementInput2); - assertEquals(newASTNode.getLocation(), this.blocks.statementInput1); + chai.assert.equal(newASTNode.getLocation(), this.blocks.statementInput1); }); test('getOutAstNodeForBlock_OneBlock', function() { var node = new Blockly.ASTNode( Blockly.ASTNode.types.BLOCK, this.blocks.statementInput4); var newASTNode = node.getOutAstNodeForBlock_(this.blocks.statementInput4); - assertEquals(newASTNode.getLocation(), this.blocks.statementInput4); + chai.assert.equal(newASTNode.getLocation(), this.blocks.statementInput4); }); test('findFirstFieldOrInput_', function() { var node = new Blockly.ASTNode( Blockly.ASTNode.types.BLOCK, this.blocks.statementInput4); var field = this.blocks.statementInput4.inputList[0].fieldRow[0]; var newASTNode = node.findFirstFieldOrInput_(this.blocks.statementInput4); - assertEquals(newASTNode.getLocation(), field); + chai.assert.equal(newASTNode.getLocation(), field); }); }); suite('NavigationFunctions', function() { + setup(function() { + Blockly.defineBlocksWithJsonArray([{ + "type": "top_connection", + "message0": "", + "previousStatement": null, + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "start_block", + "message0": "", + "nextStatement": null, + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "fields_and_input", + "message0": "%1 hi %2 %3 %4", + "args0": [ + { + "type": "field_input", + "name": "NAME", + "text": "default" + }, + { + "type": "input_dummy" + }, + { + "type": "field_input", + "name": "NAME", + "text": "default" + }, + { + "type": "input_value", + "name": "NAME" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "two_fields", + "message0": "%1 hi", + "args0": [ + { + "type": "field_input", + "name": "NAME", + "text": "default" + } + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "fields_and_input2", + "message0": "%1 %2 %3 hi %4 bye", + "args0": [ + { + "type": "input_value", + "name": "NAME" + }, + { + "type": "field_input", + "name": "NAME", + "text": "default" + }, + { + "type": "input_value", + "name": "NAME" + }, + { + "type": "input_statement", + "name": "NAME" + } + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "dummy_input", + "message0": "Hello", + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "dummy_inputValue", + "message0": "Hello %1 %2", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_value", + "name": "NAME" + } + ], + "colour": 230, + "tooltip": "", + "helpUrl": "" + }, + { + "type": "output_next", + "message0": "", + "output": null, + "colour": 230, + "tooltip": "", + "helpUrl": "", + "nextStatement": null + }]); + + var noNextConnection = this.workspace.newBlock('top_connection'); + var fieldAndInputs = this.workspace.newBlock('fields_and_input'); + var twoFields = this.workspace.newBlock('two_fields'); + var fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); + var noPrevConnection = this.workspace.newBlock('start_block'); + this.blocks.noNextConnection = noNextConnection; + this.blocks.fieldAndInputs = fieldAndInputs; + this.blocks.twoFields = twoFields; + this.blocks.fieldAndInputs2 = fieldAndInputs2; + this.blocks.noPrevConnection = noPrevConnection; + + var dummyInput = this.workspace.newBlock('dummy_input'); + var dummyInputValue = this.workspace.newBlock('dummy_inputValue'); + var fieldWithOutput2 = this.workspace.newBlock('field_input'); + this.blocks.dummyInput = dummyInput; + this.blocks.dummyInputValue = dummyInputValue; + this.blocks.fieldWithOutput2 = fieldWithOutput2; + + var secondBlock = this.workspace.newBlock('input_statement'); + var outputNextBlock = this.workspace.newBlock('output_next'); + this.blocks.secondBlock = secondBlock; + this.blocks.outputNextBlock = outputNextBlock; + + + }); + teardown(function() { + delete this.blocks.noNextConnection; + delete this.blocks.fieldAndInputs; + delete this.blocks.twoFields; + delete this.blocks.fieldAndInputs2; + delete this.blocks.noPrevConnection; + delete this.blocks.dummyInput; + delete this.blocks.dummyInputValue; + delete this.blocks.fieldWithOutput2; + + delete Blockly.Blocks['output_next']; + delete Blockly.Blocks['fields_and_input2']; + delete Blockly.Blocks['two_fields']; + delete Blockly.Blocks['fields_and_input']; + delete Blockly.Blocks['top_connection']; + delete Blockly.Blocks['start_block']; + delete Blockly.Blocks['dummy_input']; + delete Blockly.Blocks['dummy_inputValue']; + }); suite('Next', function() { setup(function() { - Blockly.defineBlocksWithJsonArray([{ - "type": "top_connection", - "message0": "", - "previousStatement": null, - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "fields_and_input", - "message0": "%1 hi %2 %3 %4", - "args0": [ - { - "type": "field_input", - "name": "NAME", - "text": "default" - }, - { - "type": "input_dummy" - }, - { - "type": "field_input", - "name": "NAME", - "text": "default" - }, - { - "type": "input_value", - "name": "NAME" - } - ], - "previousStatement": null, - "nextStatement": null, - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "two_fields", - "message0": "%1 hi", - "args0": [ - { - "type": "field_input", - "name": "NAME", - "text": "default" - } - ], - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "fields_and_input2", - "message0": "%1 %2 %3 hi %4 bye", - "args0": [ - { - "type": "input_value", - "name": "NAME" - }, - { - "type": "field_input", - "name": "NAME", - "text": "default" - }, - { - "type": "input_value", - "name": "NAME" - }, - { - "type": "input_statement", - "name": "NAME" - } - ], - "colour": 230, - "tooltip": "", - "helpUrl": "" - }]); - this.singleBlockWorkspace = new Blockly.Workspace(); - - var noNextConnection = this.workspace.newBlock('top_connection'); - var fieldAndInputs = this.workspace.newBlock('fields_and_input'); - var twoFields = this.workspace.newBlock('two_fields'); - var fieldAndInputs2 = this.workspace.newBlock('fields_and_input2'); var singleBlock = this.singleBlockWorkspace.newBlock('two_fields'); - - this.blocks.noNextConnection = noNextConnection; - this.blocks.fieldAndInputs = fieldAndInputs; - this.blocks.twoFields = twoFields; - this.blocks.fieldAndInputs2 = fieldAndInputs2; this.blocks.singleBlock = singleBlock; - - }); teardown(function() { - delete this.blocks.noNextConnection; - delete this.blocks.fieldAndInputs; - delete this.blocks.twoFields; - delete this.blocks.fieldAndInputs2; delete this.blocks.singleBlock; - - delete Blockly.Blocks['fields_and_input2']; - delete Blockly.Blocks['two_fields']; - delete Blockly.Blocks['fields_and_input']; - delete Blockly.Blocks['top_connection']; }); test('fromPreviousToBlock', function() { var prevConnection = this.blocks.statementInput1.previousConnection; var node = Blockly.ASTNode.createConnectionNode(prevConnection); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), this.blocks.statementInput1); + chai.assert.equal(nextNode.getLocation(), this.blocks.statementInput1); }); test('fromBlockToNext', function() { var nextConnection = this.blocks.statementInput1.nextConnection; var node = Blockly.ASTNode.createBlockNode(this.blocks.statementInput1); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), nextConnection); + chai.assert.equal(nextNode.getLocation(), nextConnection); }); test('fromBlockToNull', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.noNextConnection); var nextNode = node.next(); - assertEquals(nextNode, null); + chai.assert.isNull(nextNode); }); test('fromNextToPrevious', function() { var nextConnection = this.blocks.statementInput1.nextConnection; var prevConnection = this.blocks.statementInput2.previousConnection; var node = Blockly.ASTNode.createConnectionNode(nextConnection); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), prevConnection); + chai.assert.equal(nextNode.getLocation(), prevConnection); }); test('fromNextToNull', function() { var nextConnection = this.blocks.statementInput2.nextConnection; var node = Blockly.ASTNode.createConnectionNode(nextConnection); var nextNode = node.next(); - assertEquals(nextNode, null); + chai.assert.isNull(nextNode); }); test('fromInputToInput', function() { var input = this.blocks.statementInput1.inputList[0]; var inputConnection = this.blocks.statementInput1.inputList[1].connection; var node = Blockly.ASTNode.createInputNode(input); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), inputConnection); + chai.assert.equal(nextNode.getLocation(), inputConnection); }); test('fromInputToStatementInput', function() { var input = this.blocks.fieldAndInputs2.inputList[1]; var inputConnection = this.blocks.fieldAndInputs2.inputList[2].connection; var node = Blockly.ASTNode.createInputNode(input); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), inputConnection); + chai.assert.equal(nextNode.getLocation(), inputConnection); }); test('fromInputToField', function() { var input = this.blocks.fieldAndInputs2.inputList[0]; var field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; var node = Blockly.ASTNode.createInputNode(input); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), field); + chai.assert.equal(nextNode.getLocation(), field); }); test('fromInputToNull', function() { var input = this.blocks.fieldAndInputs2.inputList[2]; var node = Blockly.ASTNode.createInputNode(input); var nextNode = node.next(); - assertEquals(nextNode, null); + chai.assert.isNull(nextNode); }); test('fromOutputToBlock', function() { var output = this.blocks.fieldWithOutput.outputConnection; var node = Blockly.ASTNode.createConnectionNode(output); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), this.blocks.fieldWithOutput); + chai.assert.equal(nextNode.getLocation(), this.blocks.fieldWithOutput); }); test('fromFieldToInput', function() { var field = this.blocks.statementInput1.inputList[0].fieldRow[1]; var inputConnection = this.blocks.statementInput1.inputList[0].connection; var node = Blockly.ASTNode.createFieldNode(field); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), inputConnection); + chai.assert.equal(nextNode.getLocation(), inputConnection); }); test('fromFieldToField', function() { var field = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; var node = Blockly.ASTNode.createFieldNode(field); var field2 = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; var nextNode = node.next(); - assertEquals(nextNode.getLocation(), field2); + chai.assert.equal(nextNode.getLocation(), field2); }); test('fromFieldToNull', function() { var field = this.blocks.twoFields.inputList[0].fieldRow[0]; var node = Blockly.ASTNode.createFieldNode(field); var nextNode = node.next(); - assertEquals(nextNode, null); + chai.assert.isNull(nextNode); }); test('fromStackToStack', function() { var node = Blockly.ASTNode.createStackNode(this.blocks.statementInput1); var nextNode = node.next(); - assertEquals(nextNode.getLocation(), this.blocks.statementInput4); - assertEquals(nextNode.getType(), Blockly.ASTNode.types.STACK); + chai.assert.equal(nextNode.getLocation(), this.blocks.statementInput4); + chai.assert.equal(nextNode.getType(), Blockly.ASTNode.types.STACK); }); test('fromStackToNull', function() { var node = Blockly.ASTNode.createStackNode(this.blocks.singleBlock); var nextNode = node.next(); - assertEquals(nextNode, null); + chai.assert.isNull(nextNode); }); }); suite('Previous', function() { - setup(function() { - Blockly.defineBlocksWithJsonArray([{ - "type": "start_block", - "message0": "", - "nextStatement": null, - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "fields_and_input", - "message0": "%1 hi %2 %3 %4", - "args0": [ - { - "type": "field_input", - "name": "NAME", - "text": "default" - }, - { - "type": "input_dummy" - }, - { - "type": "field_input", - "name": "NAME", - "text": "default" - }, - { - "type": "input_value", - "name": "NAME" - } - ], - "previousStatement": null, - "nextStatement": null, - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "fields_and_input2", - "message0": "%1 %2 %3 hi %4 bye", - "args0": [ - { - "type": "input_value", - "name": "NAME" - }, - { - "type": "field_input", - "name": "NAME", - "text": "default" - }, - { - "type": "input_value", - "name": "NAME" - }, - { - "type": "input_statement", - "name": "NAME" - } - ], - "colour": 230, - "tooltip": "", - "helpUrl": "" - }]); - - var noPrevConnection = this.workspace.newBlock('start_block'); - var fieldsAndInputs = this.workspace.newBlock('fields_and_input'); - var fieldsAndInputs2 = this.workspace.newBlock('fields_and_input2'); - - this.blocks.noPrevConnection = noPrevConnection; - this.blocks.fieldsAndInputs = fieldsAndInputs; - this.blocks.fieldsAndInputs2 = fieldsAndInputs2; - }); - teardown(function() { - delete this.blocks.noPrevConnection; - delete this.blocks.fieldAndInputs; - delete this.blocks.fieldsAndInputs2; - - delete Blockly.Blocks['fields_and_input2']; - delete Blockly.Blocks['start_block']; - delete Blockly.Blocks['fields_and_input']; - }); - test('fromPreviousToNull', function() { var prevConnection = this.blocks.statementInput1.previousConnection; var node = Blockly.ASTNode.createConnectionNode(prevConnection); var prevNode = node.prev(); - assertEquals(prevNode, null); + chai.assert.isNull(prevNode); }); test('fromPreviousToNext', function() { var prevConnection = this.blocks.statementInput2.previousConnection; var node = Blockly.ASTNode.createConnectionNode(prevConnection); var prevNode = node.prev(); var nextConnection = this.blocks.statementInput1.nextConnection; - assertEquals(prevNode.getLocation(), nextConnection); + chai.assert.equal(prevNode.getLocation(), nextConnection); }); test('fromPreviousToInput', function() { var prevConnection = this.blocks.statementInput3.previousConnection; var node = Blockly.ASTNode.createConnectionNode(prevConnection); var prevNode = node.prev(); - assertEquals(prevNode, null); + chai.assert.isNull(prevNode); }); test('fromBlockToPrevious', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.statementInput1); var prevNode = node.prev(); var prevConnection = this.blocks.statementInput1.previousConnection; - assertEquals(prevNode.getLocation(), prevConnection); + chai.assert.equal(prevNode.getLocation(), prevConnection); }); test('fromBlockToNull', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.noPrevConnection); var prevNode = node.prev(); - assertEquals(prevNode, null); + chai.assert.isNull(prevNode); }); test('fromBlockToOutput', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.fieldWithOutput); var prevNode = node.prev(); var outputConnection = this.blocks.fieldWithOutput.outputConnection; - assertEquals(prevNode.getLocation(), outputConnection); + chai.assert.equal(prevNode.getLocation(), outputConnection); }); test('fromNextToBlock', function() { var nextConnection = this.blocks.statementInput1.nextConnection; var node = Blockly.ASTNode.createConnectionNode(nextConnection); var prevNode = node.prev(); - assertEquals(prevNode.getLocation(), this.blocks.statementInput1); + chai.assert.equal(prevNode.getLocation(), this.blocks.statementInput1); }); test('fromInputToField', function() { var input = this.blocks.statementInput1.inputList[0]; var node = Blockly.ASTNode.createInputNode(input); var prevNode = node.prev(); - assertEquals(prevNode.getLocation(), input.fieldRow[1]); + chai.assert.equal(prevNode.getLocation(), input.fieldRow[1]); }); test('fromInputToNull', function() { - var input = this.blocks.fieldsAndInputs2.inputList[0]; + var input = this.blocks.fieldAndInputs2.inputList[0]; var node = Blockly.ASTNode.createInputNode(input); var prevNode = node.prev(); - assertEquals(prevNode, null); + chai.assert.isNull(prevNode); }); test('fromInputToInput', function() { - var input = this.blocks.fieldsAndInputs2.inputList[2]; - var inputConnection = this.blocks.fieldsAndInputs2.inputList[1].connection; + var input = this.blocks.fieldAndInputs2.inputList[2]; + var inputConnection = this.blocks.fieldAndInputs2.inputList[1].connection; var node = Blockly.ASTNode.createInputNode(input); var prevNode = node.prev(); - assertEquals(prevNode.getLocation(), inputConnection); + chai.assert.equal(prevNode.getLocation(), inputConnection); }); test('fromOutputToNull', function() { var output = this.blocks.fieldWithOutput.outputConnection; var node = Blockly.ASTNode.createConnectionNode(output); var prevNode = node.prev(); - assertEquals(prevNode, null); + chai.assert.isNull(prevNode); }); test('fromFieldToNull', function() { var field = this.blocks.statementInput1.inputList[0].fieldRow[0]; var node = Blockly.ASTNode.createFieldNode(field); var prevNode = node.prev(); - assertEquals(prevNode, null); + chai.assert.isNull(prevNode); }); test('fromFieldToInput', function() { - var field = this.blocks.fieldsAndInputs2.inputList[1].fieldRow[0]; - var inputConnection = this.blocks.fieldsAndInputs2.inputList[0].connection; + var field = this.blocks.fieldAndInputs2.inputList[1].fieldRow[0]; + var inputConnection = this.blocks.fieldAndInputs2.inputList[0].connection; var node = Blockly.ASTNode.createFieldNode(field); var prevNode = node.prev(); - assertEquals(prevNode.getLocation(), inputConnection); + chai.assert.equal(prevNode.getLocation(), inputConnection); }); test('fromFieldToField', function() { - var field = this.blocks.fieldsAndInputs.inputList[1].fieldRow[0]; - var field2 = this.blocks.fieldsAndInputs.inputList[0].fieldRow[0]; + var field = this.blocks.fieldAndInputs.inputList[1].fieldRow[0]; + var field2 = this.blocks.fieldAndInputs.inputList[0].fieldRow[0]; var node = Blockly.ASTNode.createFieldNode(field); var prevNode = node.prev(); - assertEquals(prevNode.getLocation(), field2); + chai.assert.equal(prevNode.getLocation(), field2); }); test('fromStackToStack', function() { var node = Blockly.ASTNode.createStackNode(this.blocks.statementInput4); var prevNode = node.prev(); - assertEquals(prevNode.getLocation(), this.blocks.statementInput1); - assertEquals(prevNode.getType(), Blockly.ASTNode.types.STACK); + chai.assert.equal(prevNode.getLocation(), this.blocks.statementInput1); + chai.assert.equal(prevNode.getType(), Blockly.ASTNode.types.STACK); }); }); suite('In', function() { setup(function() { - Blockly.defineBlocksWithJsonArray([{ - "type": "dummy_input", - "message0": "Hello", - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "dummy_inputValue", - "message0": "Hello %1 %2", - "args0": [ - { - "type": "input_dummy" - }, - { - "type": "input_value", - "name": "NAME" - } - ], - "colour": 230, - "tooltip": "", - "helpUrl": "" - }]); - var dummyInput = this.workspace.newBlock('dummy_input'); - var dummyInputValue = this.workspace.newBlock('dummy_inputValue'); - var fieldWithOutput2 = this.workspace.newBlock('field_input'); - - this.blocks.dummyInput = dummyInput; - this.blocks.dummyInputValue = dummyInputValue; - this.blocks.fieldWithOutput2 = fieldWithOutput2; this.emptyWorkspace = new Blockly.Workspace(); }); teardown(function() { - delete this.blocks.dummyInput; - delete this.blocks.dummyInputValue; - delete this.blocks.fieldWithOutput2; delete this.emptyWorkspace; - - delete Blockly.Blocks['dummy_input']; - delete Blockly.Blocks['dummy_inputValue']; }); test('fromInputToOutput', function() { @@ -600,139 +546,106 @@ suite('ASTNode', function() { var node = Blockly.ASTNode.createInputNode(input); var inNode = node.in(); var outputConnection = this.blocks.fieldWithOutput.outputConnection; - assertEquals(inNode.getLocation(), outputConnection); + chai.assert.equal(inNode.getLocation(), outputConnection); }); test('fromInputToNull', function() { var input = this.blocks.statementInput2.inputList[0]; var node = Blockly.ASTNode.createInputNode(input); var inNode = node.in(); - assertEquals(inNode, null); + chai.assert.isNull(inNode); }); test('fromInputToPrevious', function() { var input = this.blocks.statementInput2.inputList[1]; var previousConnection = this.blocks.statementInput3.previousConnection; var node = Blockly.ASTNode.createInputNode(input); var inNode = node.in(); - assertEquals(inNode.getLocation(), previousConnection); + chai.assert.equal(inNode.getLocation(), previousConnection); }); test('fromBlockToInput', function() { var input = this.blocks.valueInput.inputList[0]; var node = Blockly.ASTNode.createBlockNode(this.blocks.valueInput); var inNode = node.in(); - assertEquals(inNode.getLocation(), input.connection); + chai.assert.equal(inNode.getLocation(), input.connection); }); test('fromBlockToField', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.statementInput1); var inNode = node.in(); var field = this.blocks.statementInput1.inputList[0].fieldRow[0]; - assertEquals(inNode.getLocation(), field); + chai.assert.equal(inNode.getLocation(), field); }); test('fromBlockToPrevious', function() { var prevConnection = this.blocks.statementInput4.previousConnection; var node = Blockly.ASTNode.createStackNode(this.blocks.statementInput4); var inNode = node.in(); - assertEquals(inNode.getLocation(), prevConnection); - assertEquals(inNode.getType(), Blockly.ASTNode.types.PREVIOUS); + chai.assert.equal(inNode.getLocation(), prevConnection); + chai.assert.equal(inNode.getType(), Blockly.ASTNode.types.PREVIOUS); }); test('fromBlockToNull_DummyInput', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.dummyInput); var inNode = node.in(); - assertEquals(inNode, null); + chai.assert.isNull(inNode); }); test('fromBlockToInput_DummyInputValue', function() { var node = Blockly.ASTNode.createBlockNode(this.blocks.dummyInputValue); var inputConnection = this.blocks.dummyInputValue.inputList[1].connection; var inNode = node.in(); - assertEquals(inNode.getLocation(), inputConnection); + chai.assert.equal(inNode.getLocation(), inputConnection); }); test('fromOuputToNull', function() { var output = this.blocks.fieldWithOutput.outputConnection; var node = Blockly.ASTNode.createConnectionNode(output); var inNode = node.in(); - assertEquals(inNode, null); + chai.assert.isNull(inNode); }); test('fromFieldToNull', function() { var field = this.blocks.statementInput1.inputList[0].fieldRow[0]; var node = Blockly.ASTNode.createFieldNode(field); var inNode = node.in(); - assertEquals(inNode, null); + chai.assert.isNull(inNode); }); test('fromWorkspaceToStack', function() { var coordinate = new Blockly.utils.Coordinate(100, 100); var node = Blockly.ASTNode.createWorkspaceNode(this.workspace, coordinate); var inNode = node.in(); - assertEquals(inNode.getLocation(), this.workspace.getTopBlocks()[0]); - assertEquals(inNode.getType(), Blockly.ASTNode.types.STACK); + chai.assert.equal(inNode.getLocation(), this.workspace.getTopBlocks()[0]); + chai.assert.equal(inNode.getType(), Blockly.ASTNode.types.STACK); }); test('fromWorkspaceToNull', function() { var coordinate = new Blockly.utils.Coordinate(100, 100); var node = Blockly.ASTNode.createWorkspaceNode( this.emptyWorkspace, coordinate); var inNode = node.in(); - assertEquals(inNode, null); + chai.assert.isNull(inNode); }); test('fromStackToPrevious', function() { var node = Blockly.ASTNode.createStackNode(this.blocks.statementInput1); var previous = this.blocks.statementInput1.previousConnection; var inNode = node.in(); - assertEquals(inNode.getLocation(), previous); - assertEquals(inNode.getType(), Blockly.ASTNode.types.PREVIOUS); + chai.assert.equal(inNode.getLocation(), previous); + chai.assert.equal(inNode.getType(), Blockly.ASTNode.types.PREVIOUS); }); test('fromStackToOutput', function() { var node = Blockly.ASTNode.createStackNode(this.blocks.fieldWithOutput2); var output = this.blocks.fieldWithOutput2.outputConnection; var inNode = node.in(); - assertEquals(inNode.getLocation(), output); - assertEquals(inNode.getType(), Blockly.ASTNode.types.OUTPUT); + chai.assert.equal(inNode.getLocation(), output); + chai.assert.equal(inNode.getType(), Blockly.ASTNode.types.OUTPUT); }); test('fromStackToBlock', function() { var node = Blockly.ASTNode.createStackNode(this.blocks.dummyInput); var inNode = node.in(); - assertEquals(inNode.getLocation(), this.blocks.dummyInput); - assertEquals(inNode.getType(), Blockly.ASTNode.types.BLOCK); + chai.assert.equal(inNode.getLocation(), this.blocks.dummyInput); + chai.assert.equal(inNode.getType(), Blockly.ASTNode.types.BLOCK); }); }); suite('Out', function() { setup(function() { - Blockly.defineBlocksWithJsonArray([{ - "type": "start_block", - "message0": "", - "nextStatement": null, - "colour": 230, - "tooltip": "", - "helpUrl": "" - }, - { - "type": "output_next", - "message0": "", - "output": null, - "colour": 230, - "tooltip": "", - "helpUrl": "", - "nextStatement": null - }]); - var noPrevConnection = this.workspace.newBlock('start_block'); - var secondBlock = this.workspace.newBlock('input_statement'); - var outputNextBlock = this.workspace.newBlock('output_next'); - var fieldWithOutput2 = this.workspace.newBlock('field_input'); - - noPrevConnection.nextConnection.connect(secondBlock.previousConnection); + var secondBlock = this.blocks.secondBlock; + var outputNextBlock = this.blocks.outputNextBlock; + this.blocks.noPrevConnection.nextConnection.connect(secondBlock.previousConnection); secondBlock.inputList[0].connection .connect(outputNextBlock.outputConnection); - this.blocks.noPrevConnection = noPrevConnection; - this.blocks.secondBlock = secondBlock; - this.blocks.outputNextBlock = outputNextBlock; - this.blocks.fieldWithOutput2 = fieldWithOutput2; - }); - teardown(function() { - delete this.blocks.noPrevConnection; - delete this.blocks.secondBlock; - delete this.blocks.outputNextBlock; - delete this.blocks.fieldWithOutput2; - - delete Blockly.Blocks['start_block']; - delete Blockly.Blocks['output_next']; }); test('fromInputToBlock', function() { @@ -861,40 +774,55 @@ suite('ASTNode', function() { test('createFieldNode', function() { var field = this.blocks.statementInput1.inputList[0].fieldRow[0]; var node = Blockly.ASTNode.createFieldNode(field); - assertEquals(node.getLocation(), field); - assertEquals(node.getType(), Blockly.ASTNode.types.FIELD); - assertFalse(node.isConnection()); + chai.assert.equal(node.getLocation(), field); + chai.assert.equal(node.getType(), Blockly.ASTNode.types.FIELD); + chai.assert.isFalse(node.isConnection()); }); test('createConnectionNode', function() { var prevConnection = this.blocks.statementInput4.previousConnection; var node = Blockly.ASTNode.createConnectionNode(prevConnection); - assertEquals(node.getLocation(), prevConnection); - assertEquals(node.getType(), Blockly.ASTNode.types.PREVIOUS); - assertTrue(node.isConnection()); + chai.assert.equal(node.getLocation(), prevConnection); + chai.assert.equal(node.getType(), Blockly.ASTNode.types.PREVIOUS); + chai.assert.isTrue(node.isConnection()); }); test('createInputNode', function() { var input = this.blocks.statementInput1.inputList[0]; var node = Blockly.ASTNode.createInputNode(input); - assertEquals(node.getLocation(), input.connection); - assertEquals(node.getType(), Blockly.ASTNode.types.INPUT); - assertTrue(node.isConnection()); + chai.assert.equal(node.getLocation(), input.connection); + chai.assert.equal(node.getType(), Blockly.ASTNode.types.INPUT); + chai.assert.isTrue(node.isConnection()); }); test('createWorkspaceNode', function() { var coordinate = new Blockly.utils.Coordinate(100, 100); var node = Blockly.ASTNode .createWorkspaceNode(this.workspace, coordinate); - assertEquals(node.getLocation(), this.workspace); - assertEquals(node.getType(), Blockly.ASTNode.types.WORKSPACE); - assertEquals(node.getWsCoordinate(), coordinate); - assertFalse(node.isConnection()); + chai.assert.equal(node.getLocation(), this.workspace); + chai.assert.equal(node.getType(), Blockly.ASTNode.types.WORKSPACE); + chai.assert.equal(node.getWsCoordinate(), coordinate); + chai.assert.isFalse(node.isConnection()); }); test('createStatementConnectionNode', function() { var nextConnection = this.blocks.statementInput1.inputList[1].connection; var inputConnection = this.blocks.statementInput1.inputList[1].connection; var node = Blockly.ASTNode.createConnectionNode(nextConnection); - assertEquals(node.getLocation(), inputConnection); - assertEquals(node.getType(), Blockly.ASTNode.types.INPUT); - assertTrue(node.isConnection()); + chai.assert.equal(node.getLocation(), inputConnection); + chai.assert.equal(node.getType(), Blockly.ASTNode.types.INPUT); + chai.assert.isTrue(node.isConnection()); + }); + test('createTopNode-previous', function() { + var block = this.blocks.statementInput1; + var topNode = Blockly.ASTNode.createTopNode(block); + chai.assert.equal(topNode.getLocation(), block.previousConnection); + }); + test('createTopNode-block', function() { + var block = this.blocks.noPrevConnection; + var topNode = Blockly.ASTNode.createTopNode(block); + chai.assert.equal(topNode.getLocation(), block); + }); + test('createTopNode-output', function() { + var block = this.blocks.outputNextBlock; + var topNode = Blockly.ASTNode.createTopNode(block); + chai.assert.equal(topNode.getLocation(), block.outputConnection); }); }); }); diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index 69a00b96e..3375c57bb 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -44,56 +44,67 @@ suite('Blocks', function() { }); teardown(function() { this.workspace.dispose(); + delete Blockly.Blocks['empty_block']; delete Blockly.Blocks['stack_block']; delete Blockly.Blocks['row_block']; delete Blockly.Blocks['statement_block']; }); + function createTestBlocks(workspace, isRow) { + var blockType = isRow ? 'row_block' : 'stack_block'; + var blockA = workspace.newBlock(blockType); + var blockB = workspace.newBlock(blockType); + var blockC = workspace.newBlock(blockType); + + if (isRow) { + blockA.inputList[0].connection.connect(blockB.outputConnection); + blockB.inputList[0].connection.connect(blockC.outputConnection); + } else { + blockA.nextConnection.connect(blockB.previousConnection); + blockB.nextConnection.connect(blockC.previousConnection); + } + + chai.assert.equal(blockC.getParent(), blockB); + + return { + A: blockA, /* Parent */ + B: blockB, /* Middle */ + C: blockC /* Child */ + }; + } + suite('Unplug', function() { function assertUnpluggedNoheal(blocks) { // A has nothing connected to it. - assertEquals(0, blocks.A.getChildren().length); + chai.assert.equal(blocks.A.getChildren().length, 0); // B and C are still connected. - assertEquals(blocks.B, blocks.C.getParent()); + chai.assert.equal(blocks.C.getParent(), blocks.B); // B is the top of its stack. - assertNull(blocks.B.getParent()); + chai.assert.isNull(blocks.B.getParent()); } function assertUnpluggedHealed(blocks) { // A and C are connected. - assertEquals(1, blocks.A.getChildren().length); - assertEquals(blocks.A, blocks.C.getParent()); + chai.assert.equal(blocks.A.getChildren().length, 1); + chai.assert.equal(blocks.C.getParent(), blocks.A); // B has nothing connected to it. - assertEquals(0, blocks.B.getChildren().length); + chai.assert.equal(blocks.B.getChildren().length, 0); // B is the top of its stack. - assertNull(blocks.B.getParent()); + chai.assert.isNull(blocks.B.getParent()); } function assertUnpluggedHealFailed(blocks) { // A has nothing connected to it. - assertEquals(0, blocks.A.getChildren().length); + chai.assert.equal(blocks.A.getChildren().length, 0); // B has nothing connected to it. - assertEquals(0, blocks.B.getChildren().length); + chai.assert.equal(blocks.B.getChildren().length, 0); // B is the top of its stack. - assertNull(blocks.B.getParent()); + chai.assert.isNull(blocks.B.getParent()); // C is the top of its stack. - assertNull(blocks.C.getParent()); + chai.assert.isNull(blocks.C.getParent()); } suite('Row', function() { setup(function() { - var blockA = this.workspace.newBlock('row_block'); - var blockB = this.workspace.newBlock('row_block'); - var blockC = this.workspace.newBlock('row_block'); - - blockA.inputList[0].connection.connect(blockB.outputConnection); - blockB.inputList[0].connection.connect(blockC.outputConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; + this.blocks = createTestBlocks(this.workspace, true); }); test('Don\'t heal', function() { @@ -116,21 +127,21 @@ suite('Blocks', function() { blocks.B.unplug(true); assertUnpluggedHealFailed(blocks); }); - test('A has multiple inputs', function() { + test('Parent has multiple inputs', function() { var blocks = this.blocks; // Add extra input to parent blocks.A.appendValueInput("INPUT").setCheck(null); blocks.B.unplug(true); assertUnpluggedHealed(blocks); }); - test('B has multiple inputs', function() { + test('Middle has multiple inputs', function() { var blocks = this.blocks; // Add extra input to middle block blocks.B.appendValueInput("INPUT").setCheck(null); blocks.B.unplug(true); assertUnpluggedHealed(blocks); }); - test('C has multiple inputs', function() { + test('Child has multiple inputs', function() { var blocks = this.blocks; // Add extra input to child block blocks.C.appendValueInput("INPUT").setCheck(null); @@ -138,7 +149,7 @@ suite('Blocks', function() { blocks.B.unplug(true); assertUnpluggedHealed(blocks); }); - test('C is Shadow', function() { + test('Child is shadow', function() { var blocks = this.blocks; blocks.C.setShadow(true); blocks.B.unplug(true); @@ -149,20 +160,7 @@ suite('Blocks', function() { }); suite('Stack', function() { setup(function() { - var blockA = this.workspace.newBlock('stack_block'); - var blockB = this.workspace.newBlock('stack_block'); - var blockC = this.workspace.newBlock('stack_block'); - - blockA.nextConnection.connect(blockB.previousConnection); - blockB.nextConnection.connect(blockC.previousConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; + this.blocks = createTestBlocks(this.workspace, false); }); test('Don\'t heal', function() { @@ -184,7 +182,7 @@ suite('Blocks', function() { assertUnpluggedHealFailed(blocks); }); - test('C is Shadow', function() { + test('Child is shadow', function() { var blocks = this.blocks; blocks.C.setShadow(true); blocks.B.unplug(true); @@ -198,7 +196,7 @@ suite('Blocks', function() { function assertDisposedNoheal(blocks) { chai.assert.isFalse(blocks.A.disposed); // A has nothing connected to it. - chai.assert.equal(0, blocks.A.getChildren().length); + chai.assert.equal(blocks.A.getChildren().length, 0); // B is disposed. chai.assert.isTrue(blocks.B.disposed); // And C is disposed. @@ -208,8 +206,8 @@ suite('Blocks', function() { chai.assert.isFalse(blocks.A.disposed); chai.assert.isFalse(blocks.C.disposed); // A and C are connected. - assertEquals(1, blocks.A.getChildren().length); - assertEquals(blocks.A, blocks.C.getParent()); + chai.assert.equal(blocks.A.getChildren().length, 1); + chai.assert.equal(blocks.C.getParent(), blocks.A); // B is disposed. chai.assert.isTrue(blocks.B.disposed); } @@ -217,29 +215,16 @@ suite('Blocks', function() { chai.assert.isFalse(blocks.A.disposed); chai.assert.isFalse(blocks.C.disposed); // A has nothing connected to it. - chai.assert.equal(0, blocks.A.getChildren().length); + chai.assert.equal(blocks.A.getChildren().length, 0); // B is disposed. chai.assert.isTrue(blocks.B.disposed); // C is the top of its stack. - assertNull(blocks.C.getParent()); + chai.assert.isNull(blocks.C.getParent()); } suite('Row', function() { setup(function() { - var blockA = this.workspace.newBlock('row_block'); - var blockB = this.workspace.newBlock('row_block'); - var blockC = this.workspace.newBlock('row_block'); - - blockA.inputList[0].connection.connect(blockB.outputConnection); - blockB.inputList[0].connection.connect(blockC.outputConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; + this.blocks = createTestBlocks(this.workspace, true); }); test('Don\'t heal', function() { @@ -262,21 +247,21 @@ suite('Blocks', function() { blocks.B.dispose(true); assertDisposedHealFailed(blocks); }); - test('A has multiple inputs', function() { + test('Parent has multiple inputs', function() { var blocks = this.blocks; // Add extra input to parent blocks.A.appendValueInput("INPUT").setCheck(null); blocks.B.dispose(true); assertDisposedHealed(blocks); }); - test('B has multiple inputs', function() { + test('Middle has multiple inputs', function() { var blocks = this.blocks; // Add extra input to middle block blocks.B.appendValueInput("INPUT").setCheck(null); blocks.B.dispose(true); assertDisposedHealed(blocks); }); - test('C has multiple inputs', function() { + test('Child has multiple inputs', function() { var blocks = this.blocks; // Add extra input to child block blocks.C.appendValueInput("INPUT").setCheck(null); @@ -284,7 +269,7 @@ suite('Blocks', function() { blocks.B.dispose(true); assertDisposedHealed(blocks); }); - test('C is Shadow', function() { + test('Child is shadow', function() { var blocks = this.blocks; blocks.C.setShadow(true); blocks.B.dispose(true); @@ -295,20 +280,7 @@ suite('Blocks', function() { }); suite('Stack', function() { setup(function() { - var blockA = this.workspace.newBlock('stack_block'); - var blockB = this.workspace.newBlock('stack_block'); - var blockC = this.workspace.newBlock('stack_block'); - - blockA.nextConnection.connect(blockB.previousConnection); - blockB.nextConnection.connect(blockC.previousConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; + this.blocks = createTestBlocks(this.workspace, false); }); test('Don\'t heal', function() { @@ -330,7 +302,7 @@ suite('Blocks', function() { assertDisposedHealFailed(blocks); }); - test('C is Shadow', function() { + test('Child is shadow', function() { var blocks = this.blocks; blocks.C.setShadow(true); blocks.B.dispose(true); @@ -353,21 +325,10 @@ suite('Blocks', function() { } ] }, - { - "type": "statement_block", - "message0": "%1", - "args0": [ - { - "type": "input_statement", - "name": "STATEMENT" - } - ] - }, ]); }); teardown(function() { delete Blockly.Blocks['value_block']; - delete Blockly.Blocks['statement_block']; }); suite('Value', function() { @@ -1059,17 +1020,9 @@ suite('Blocks', function() { }); suite('Comments', function() { setup(function() { - Blockly.defineBlocksWithJsonArray([ - { - "type": "empty_block", - "message0": "", - "args0": [] - }, - ]); this.eventSpy = sinon.spy(Blockly.Events, 'fire'); }); teardown(function() { - delete Blockly.Blocks['empty_block']; this.eventSpy.restore(); }); suite('Set/Get Text', function() { @@ -1104,14 +1057,14 @@ suite('Blocks', function() { }); test('Text Null', function() { this.block.setCommentText(null); - chai.assert.equal(this.block.getCommentText(), null); + chai.assert.isNull(this.block.getCommentText()); assertNoCommentEvent(this.eventSpy); }); test('Text -> Null', function() { this.block.setCommentText('first text'); this.block.setCommentText(null); - chai.assert.equal(this.block.getCommentText(), null); + chai.assert.isNull(this.block.getCommentText()); assertCommentEvent(this.eventSpy, 'first text', null); }); }); @@ -1140,14 +1093,14 @@ suite('Blocks', function() { }); test('Text Null', function() { this.block.setCommentText(null); - chai.assert.equal(this.block.getCommentText(), null); + chai.assert.isNull(this.block.getCommentText()); assertNoCommentEvent(this.eventSpy); }); test('Text -> Null', function() { this.block.setCommentText('first text'); this.block.setCommentText(null); - chai.assert.equal(this.block.getCommentText(), null); + chai.assert.isNull(this.block.getCommentText()); assertCommentEvent(this.eventSpy, 'first text', null); }); test('Set While Visible - Editable', function() { @@ -1238,7 +1191,434 @@ suite('Blocks', function() { }); }); }); + suite('Collapsing and Expanding', function() { + function assertCollapsed(block, opt_string) { + chai.assert.isTrue(block.isCollapsed()); + for (var i = 0, input; (input = block.inputList[i]); i++) { + if (input.name == Blockly.Block.COLLAPSED_INPUT_NAME) { + continue; + } + chai.assert.isFalse(input.isVisible()); + for (var j = 0, field; (field = input.fieldRow[j]); j++) { + chai.assert.isFalse(field.isVisible()); + } + } + var icons = block.getIcons(); + for (var i = 0, icon; (icon = icons[i]); i++) { + chai.assert.isFalse(icon.isVisible()); + } + var input = block.getInput(Blockly.Block.COLLAPSED_INPUT_NAME); + chai.assert.isNotNull(input); + chai.assert.isTrue(input.isVisible()); + var field = block.getField(Blockly.Block.COLLAPSED_FIELD_NAME); + chai.assert.isNotNull(field); + chai.assert.isTrue(field.isVisible()); + + if (opt_string) { + chai.assert.equal(field.getText(), opt_string); + } + } + function assertNotCollapsed(block) { + chai.assert.isFalse(block.isCollapsed()); + for (var i = 0, input; (input = block.inputList[i]); i++) { + chai.assert.isTrue(input.isVisible()); + for (var j = 0, field; (field = input.fieldRow[j]); j++) { + chai.assert.isTrue(field.isVisible()); + } + } + + var input = block.getInput(Blockly.Block.COLLAPSED_INPUT_NAME); + chai.assert.isNull(input); + var field = block.getField(Blockly.Block.COLLAPSED_FIELD_NAME); + chai.assert.isNull(field); + } + function isBlockHidden(block) { + var node = block.getSvgRoot(); + do { + var visible = node.style.display != 'none'; + if (!visible) { + return true; + } + node = node.parentNode; + } while (node != document); + return false; + } + + setup(function() { + Blockly.Events.disable(); + // We need a visible workspace. + this.workspace = Blockly.inject('blocklyDiv', {}); + Blockly.defineBlocksWithJsonArray([ + { + "type": "variable_block", + "message0": "%1", + "args0": [ + { + "type": "field_variable", + "name": "NAME", + "variable": "x" + } + ], + } + ]); + this.createBlock = function(type) { + var block = this.workspace.newBlock(type); + block.initSvg(); + block.render(); + return block; + }; + }); + teardown(function() { + Blockly.Events.enable(); + delete Blockly.Blocks['variable_block']; + }); + suite('Connecting and Disconnecting', function() { + test('Connect Block to Next', function() { + var blockA = this.createBlock('stack_block'); + var blockB = this.createBlock('stack_block'); + + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.nextConnection.connect(blockB.previousConnection); + assertNotCollapsed(blockB); + }); + test('Connect Block to Value Input', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + chai.assert.isTrue(isBlockHidden(blockB)); + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + }); + test('Connect Block to Statement Input', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + chai.assert.isTrue(isBlockHidden(blockB)); + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + }); + test('Connect Block to Child of Collapsed - Input', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + var blockC = this.createBlock('row_block'); + + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + blockB.getInput('INPUT').connection.connect(blockC.outputConnection); + chai.assert.isTrue(isBlockHidden(blockC)); + + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Connect Block to Child of Collapsed - Next', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + var blockC = this.createBlock('stack_block'); + + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + blockB.nextConnection.connect(blockC.previousConnection); + chai.assert.isTrue(isBlockHidden(blockC)); + + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Connect Block to Value Input Already Taken', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + var blockC = this.createBlock('row_block'); + + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + blockA.getInput('INPUT').connection.connect(blockC.outputConnection); + chai.assert.isTrue(isBlockHidden(blockC)); + // Still hidden after C is inserted between. + chai.assert.isTrue(isBlockHidden(blockB)); + + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Connect Block to Statement Input Already Taken', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + var blockC = this.createBlock('stack_block'); + + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + blockA.getInput('STATEMENT').connection + .connect(blockC.previousConnection); + chai.assert.isTrue(isBlockHidden(blockC)); + // Still hidden after C is inserted between. + chai.assert.isTrue(isBlockHidden(blockB)); + + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Connect Block with Child - Input', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + var blockC = this.createBlock('row_block'); + + blockB.getInput('INPUT').connection.connect(blockC.outputConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + chai.assert.isTrue(isBlockHidden(blockC)); + chai.assert.isTrue(isBlockHidden(blockB)); + + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Connect Block with Child - Statement', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + var blockC = this.createBlock('stack_block'); + + blockB.nextConnection.connect(blockC.previousConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + chai.assert.isTrue(isBlockHidden(blockC)); + chai.assert.isTrue(isBlockHidden(blockB)); + + blockA.setCollapsed(false); + assertNotCollapsed(blockA); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Disconnect Block from Value Input', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + blockB.outputConnection.disconnect(); + chai.assert.isFalse(isBlockHidden(blockB)); + }); + test('Disconnect Block from Statement Input', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + blockB.previousConnection.disconnect(); + chai.assert.isFalse(isBlockHidden(blockB)); + }); + test('Disconnect Block from Child of Collapsed - Input', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + var blockC = this.createBlock('row_block'); + + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + blockB.getInput('INPUT').connection.connect(blockC.outputConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + chai.assert.isTrue(isBlockHidden(blockC)); + + blockC.outputConnection.disconnect(); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Disconnect Block from Child of Collapsed - Next', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + var blockC = this.createBlock('stack_block'); + + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + blockB.nextConnection.connect(blockC.previousConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + chai.assert.isTrue(isBlockHidden(blockC)); + + blockC.previousConnection.disconnect(); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Disconnect Block with Child - Input', function() { + var blockA = this.createBlock('row_block'); + var blockB = this.createBlock('row_block'); + var blockC = this.createBlock('row_block'); + + blockB.getInput('INPUT').connection.connect(blockC.outputConnection); + blockA.getInput('INPUT').connection.connect(blockB.outputConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockB)); + chai.assert.isTrue(isBlockHidden(blockC)); + + blockB.outputConnection.disconnect(); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + test('Disconnect Block with Child - Statement', function() { + var blockA = this.createBlock('statement_block'); + var blockB = this.createBlock('stack_block'); + var blockC = this.createBlock('stack_block'); + + blockB.nextConnection.connect(blockC.previousConnection); + blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); + blockA.setCollapsed(true); + assertCollapsed(blockA); + chai.assert.isTrue(isBlockHidden(blockC)); + chai.assert.isTrue(isBlockHidden(blockB)); + + blockB.previousConnection.disconnect(); + chai.assert.isFalse(isBlockHidden(blockB)); + chai.assert.isFalse(isBlockHidden(blockC)); + }); + }); + suite('Adding and Removing Block Parts', function() { + test('Add Previous Connection', function() { + var blockA = this.createBlock('empty_block'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.setPreviousStatement(true); + assertCollapsed(blockA); + chai.assert.isNotNull(blockA.previousConnection); + }); + test('Add Next Connection', function() { + var blockA = this.createBlock('empty_block'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.setNextStatement(true); + assertCollapsed(blockA); + chai.assert.isNotNull(blockA.nextConnection); + }); + test('Add Input', function() { + var blockA = this.createBlock('empty_block'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.appendDummyInput('NAME'); + assertCollapsed(blockA); + chai.assert.isNotNull(blockA.getInput('NAME')); + }); + test('Add Field', function() { + var blockA = this.createBlock('empty_block'); + var input = blockA.appendDummyInput('NAME'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + input.appendField(new Blockly.FieldLabel('test'), 'FIELD'); + assertCollapsed(blockA); + var field = blockA.getField('FIELD'); + chai.assert.isNotNull(field); + chai.assert.equal(field.getText(), 'test'); + }); + test('Add Icon', function() { + var blockA = this.createBlock('empty_block'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.setCommentText('test'); + assertCollapsed(blockA); + }); + test('Remove Previous Connection', function() { + var blockA = this.createBlock('empty_block'); + blockA.setPreviousStatement(true); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.setPreviousStatement(false); + assertCollapsed(blockA); + chai.assert.isNull(blockA.previousConnection); + }); + test('Remove Next Connection', function() { + var blockA = this.createBlock('empty_block'); + blockA.setNextStatement(true); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.setNextStatement(false); + assertCollapsed(blockA); + chai.assert.isNull(blockA.nextConnection); + }); + test('Remove Input', function() { + var blockA = this.createBlock('empty_block'); + blockA.appendDummyInput('NAME'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.removeInput('NAME'); + assertCollapsed(blockA); + chai.assert.isNull(blockA.getInput('NAME')); + }); + test('Remove Field', function() { + var blockA = this.createBlock('empty_block'); + var input = blockA.appendDummyInput('NAME'); + input.appendField(new Blockly.FieldLabel('test'), 'FIELD'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + input.removeField('FIELD'); + assertCollapsed(blockA); + var field = blockA.getField('FIELD'); + chai.assert.isNull(field); + }); + test('Remove Icon', function() { + var blockA = this.createBlock('empty_block'); + blockA.setCommentText('test'); + blockA.setCollapsed(true); + assertCollapsed(blockA); + blockA.setCommentText(null); + assertCollapsed(blockA); + }); + }); + suite('Renaming Vars', function() { + test('Simple Rename', function() { + var blockA = this.createBlock('variable_block'); + + blockA.setCollapsed(true); + assertCollapsed(blockA, 'x'); + + var variable = this.workspace.getVariable('x', ''); + this.workspace.renameVariableById(variable.getId(), 'y'); + assertCollapsed(blockA, 'y'); + }); + test('Coalesce, Different Case', function() { + var blockA = this.createBlock('variable_block'); + + blockA.setCollapsed(true); + assertCollapsed(blockA, 'x'); + + var variable = this.workspace.createVariable('y'); + this.workspace.renameVariableById(variable.getId(), 'X'); + assertCollapsed(blockA, 'X'); + }); + }); + }); suite('Style', function() { suite('Headless', function() { setup(function() { @@ -1248,16 +1628,16 @@ suite('Blocks', function() { }); test('Set colour', function() { this.block.setColour('20'); - assertEquals(this.block.getColour(), '#a5745b'); - assertEquals(this.block.colour_, this.block.getColour()); - assertEquals(this.block.hue_, '20'); + chai.assert.equal(this.block.getColour(), '#a5745b'); + chai.assert.equal(this.block.colour_, this.block.getColour()); + chai.assert.equal(this.block.hue_, '20'); }); test('Set style', function() { this.block.setStyle('styleOne'); - assertEquals(this.block.getStyleName(), 'styleOne'); - assertEquals(this.block.hue_, null); + chai.assert.equal(this.block.getStyleName(), 'styleOne'); + chai.assert.isNull(this.block.hue_); // Calling setStyle does not update the colour on a headless block. - assertEquals(this.block.getColour(), '#000000'); + chai.assert.equal(this.block.getColour(), '#000000'); }); }); suite('Rendered', function() { @@ -1277,26 +1657,140 @@ suite('Blocks', function() { }); teardown(function() { this.workspace.dispose(); + // Clear all registered themes. + Blockly.registry.typeMap_['theme'] = {}; }); test('Set colour hue', function() { this.block.setColour('20'); - assertEquals(this.block.getStyleName(), 'auto_#a5745b'); - assertEquals(this.block.getColour(), '#a5745b'); - assertEquals(this.block.colour_, this.block.getColour()); - assertEquals(this.block.hue_, '20'); + chai.assert.equal(this.block.getStyleName(), 'auto_#a5745b'); + chai.assert.equal(this.block.getColour(), '#a5745b'); + chai.assert.equal(this.block.colour_, this.block.getColour()); + chai.assert.equal(this.block.hue_, '20'); }); test('Set colour hex', function() { this.block.setColour('#000000'); - assertEquals(this.block.getStyleName(), 'auto_#000000'); - assertEquals(this.block.getColour(), '#000000'); - assertEquals(this.block.colour_, this.block.getColour()); - assertEquals(this.block.hue_, null); + chai.assert.equal(this.block.getStyleName(), 'auto_#000000'); + chai.assert.equal(this.block.getColour(), '#000000'); + chai.assert.equal(this.block.colour_, this.block.getColour()); + chai.assert.isNull(this.block.hue_); }); test('Set style', function() { this.block.setStyle('styleOne'); - assertEquals(this.block.getStyleName(), 'styleOne'); - assertEquals(this.block.getColour(), '#000000'); - assertEquals(this.block.colour_, this.block.getColour()); + chai.assert.equal(this.block.getStyleName(), 'styleOne'); + chai.assert.equal(this.block.getColour(), '#000000'); + chai.assert.equal(this.block.colour_, this.block.getColour()); + }); + }); + }); + suite('toString', function() { + var toStringTests = [ + { + name: 'statement block', + xml: '' + + '' + + '' + + '10' + + '' + + '' + + '', + toString: 'repeat 10 times do ?', + }, + { + name: 'nested statement blocks', + xml: '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '' + + '', + toString: 'repeat 10 times do if ? do ?', + }, + { + name: 'nested Boolean output blocks', + xml: '' + + '' + + '' + + 'EQ' + + '' + + '' + + 'AND' + + '' + + '' + + '' + + '' + + '', + toString: 'if ((? and ?) = ?) do ?', + }, + { + name: 'output block', + xml: '' + + 'ROOT' + + '' + + '' + + '9' + + '' + + '' + + '', + toString: 'square root 9', + }, + { + name: 'nested Number output blocks', + xml: '' + + 'ADD' + + '' + + '' + + '1' + + '' + + '' + + 'MULTIPLY' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '5' + + '' + + '' + + '' + + '' + + '' + + '' + + '3' + + '' + + '' + + '', + toString: '(10 × 5) + 3', + }, + { + name: 'nested String output blocks', + xml: '' + + '' + + '' + + '' + + 'Hello' + + '' + + '' + + '' + + '' + + 'World' + + '' + + '' + + '', + toString: 'create text with “ Hello ” “ World ”', + }, + ]; + // Create mocha test cases for each toString test. + toStringTests.forEach(function(t) { + test(t.name, function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(t.xml), + this.workspace); + chai.assert.equal(block.toString(), t.toString); }); }); }); diff --git a/tests/mocha/cursor_test.js b/tests/mocha/cursor_test.js index bc367e09b..6286ff283 100644 --- a/tests/mocha/cursor_test.js +++ b/tests/mocha/cursor_test.js @@ -83,14 +83,14 @@ suite('Cursor', function() { this.cursor.setCurNode(prevNode); this.cursor.next(); var curNode = this.cursor.getCurNode(); - assertEquals(curNode.getLocation(), this.blocks.B.previousConnection); + chai.assert.equal(curNode.getLocation(), this.blocks.B.previousConnection); }); test('Next - From last block in a stack go to next connection', function() { var prevNode = Blockly.ASTNode.createConnectionNode(this.blocks.B.previousConnection); this.cursor.setCurNode(prevNode); this.cursor.next(); var curNode = this.cursor.getCurNode(); - assertEquals(curNode.getLocation(), this.blocks.B.nextConnection); + chai.assert.equal(curNode.getLocation(), this.blocks.B.nextConnection); }); test('In - From output connection', function() { @@ -99,7 +99,7 @@ suite('Cursor', function() { this.cursor.setCurNode(outputNode); this.cursor.in(); var curNode = this.cursor.getCurNode(); - assertEquals(curNode.getLocation(), fieldBlock.inputList[0].fieldRow[0]); + chai.assert.equal(curNode.getLocation(), fieldBlock.inputList[0].fieldRow[0]); }); test('Prev - From previous connection skip over next connection', function() { @@ -108,7 +108,7 @@ suite('Cursor', function() { this.cursor.setCurNode(prevConnectionNode); this.cursor.prev(); var curNode = this.cursor.getCurNode(); - assertEquals(curNode.getLocation(), this.blocks.A.previousConnection); + chai.assert.equal(curNode.getLocation(), this.blocks.A.previousConnection); }); test('Out - From field skip over block node', function() { @@ -117,6 +117,6 @@ suite('Cursor', function() { this.cursor.setCurNode(fieldNode); this.cursor.out(); var curNode = this.cursor.getCurNode(); - assertEquals(curNode.getLocation(), this.blocks.E.outputConnection); + chai.assert.equal(curNode.getLocation(), this.blocks.E.outputConnection); }); }); diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index 4b157925f..6cf675875 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -38,32 +38,42 @@ suite('Events', function() { var keys = Object.keys(values); for (var i = 0; i < keys.length; i++) { var field = keys[i]; - assertEquals(values[field], event[field]); + chai.assert.equal(values[field], event[field]); } } function checkCreateEventValues(event, block, ids, type) { var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block)); var result_xml = Blockly.Xml.domToText(event.xml); - assertEquals(expected_xml, result_xml); - isEqualArrays(ids, event.ids); - assertEquals(type, event.type); + chai.assert.equal(expected_xml, result_xml); + chai.assert.deepEqual(ids, event.ids); + chai.assert.equal(type, event.type); } function checkDeleteEventValues(event, block, ids, type) { var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block)); var result_xml = Blockly.Xml.domToText(event.oldXml); - assertEquals(expected_xml, result_xml); - isEqualArrays(ids, event.ids); - assertEquals(type, event.type); + chai.assert.equal(expected_xml, result_xml); + chai.assert.deepEqual(ids, event.ids); + chai.assert.equal(type, event.type); + } + + function createSimpleTestBlock(workspace, opt_prototypeName) { + // Disable events while constructing the block: this is a test of the + // Blockly.Event constructors, not the block constructor.s + Blockly.Events.disable(); + var block = new Blockly.Block( + workspace, opt_prototypeName || 'simple_test_block'); + Blockly.Events.enable(); + return block; } suite('Constructors', function() { test('Abstract', function() { var event = new Blockly.Events.Abstract(); - assertUndefined(event.blockId); - assertUndefined(event.workspaceId); - assertUndefined(event.varId); + chai.assert.isUndefined(event.blockId); + chai.assert.isUndefined(event.workspaceId); + chai.assert.isUndefined(event.varId); checkExactEventValues(event, {'group': '', 'recordUndo': true}); }); @@ -87,12 +97,7 @@ suite('Events', function() { setup(function() { this.FAKE_ID = 'hedgehog'; sinon.stub(Blockly.utils, "genUid").returns(this.FAKE_ID); - - // Disable events while constructing the block: this is a test of the - // Blockly.Event constructors, not the block constructor. - Blockly.Events.disable(); - this.block = new Blockly.Block(this.workspace, 'simple_test_block'); - Blockly.Events.enable(); + this.block = createSimpleTestBlock(this.workspace); sinon.restore(); }); @@ -101,7 +106,7 @@ suite('Events', function() { test('Block base', function() { var event = new Blockly.Events.BlockBase(this.block); - assertUndefined(event.varId); + chai.assert.isUndefined(event.varId); checkExactEventValues(event, { 'blockId': this.FAKE_ID, @@ -169,9 +174,7 @@ suite('Events', function() { suite('Move by parent', function() { setup(function() { sinon.stub(Blockly.utils, "genUid").returns("parent"); - Blockly.Events.disable(); - this.parentBlock = new Blockly.Block(this.workspace, 'simple_test_block'); - Blockly.Events.enable(); + this.parentBlock = createSimpleTestBlock(this.workspace); sinon.restore(); this.block.parentBlock_ = this.parentBlock; @@ -201,15 +204,7 @@ suite('Events', function() { suite('With variable getter blocks', function() { setup(function() { - // Disable events while constructing the block: this is a test of the - // Blockly.Event constructors, not the block constructor. - Blockly.Events.disable(); - this.block = new Blockly.Block(this.workspace, 'field_variable_test_block'); - Blockly.Events.enable(); - }); - - teardown(function() { - + this.block = createSimpleTestBlock(this.workspace, 'field_variable_test_block'); }); test('Change', function() { @@ -243,15 +238,16 @@ suite('Events', function() { */ function checkVariableValues(container, name, type, id) { var variable = container.getVariableById(id); - assertNotUndefined(variable); - assertEquals(name, variable.name); - assertEquals(type, variable.type); - assertEquals(id, variable.getId()); + chai.assert.isDefined(variable); + chai.assert.equal(name, variable.name); + chai.assert.equal(type, variable.type); + chai.assert.equal(id, variable.getId()); } + suite('Constructors', function() { test('Var base', function() { var event = new Blockly.Events.VarBase(this.variable); - assertUndefined(event.blockId); + chai.assert.isUndefined(event.blockId); checkExactEventValues(event, {'varId': 'id1', 'workspaceId': this.workspace.id, 'group': '', 'recordUndo': true}); }); @@ -282,7 +278,7 @@ suite('Events', function() { var json = event.toJson(); event2.fromJson(json); - assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); + chai.assert.equal(JSON.stringify(json), JSON.stringify(event2.toJson())); }); test('Var delete', function() { var event = new Blockly.Events.VarDelete(this.variable); @@ -290,7 +286,7 @@ suite('Events', function() { var json = event.toJson(); event2.fromJson(json); - assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); + chai.assert.equal(JSON.stringify(json), JSON.stringify(event2.toJson())); }); test('Var rename', function() { var event = new Blockly.Events.VarRename(this.variable, ''); @@ -298,19 +294,18 @@ suite('Events', function() { var json = event.toJson(); event2.fromJson(json); - assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); + chai.assert.equal(JSON.stringify(json), JSON.stringify(event2.toJson())); }); }); suite('toJson', function() { test('Var create', function() { - var variable = this.workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarCreate(variable); + var event = new Blockly.Events.VarCreate(this.variable); var json = event.toJson(); var expectedJson = ({type: "var_create", varId: "id1", varType: "type1", varName: "name1"}); - assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); + chai.assert.equal(JSON.stringify(expectedJson), JSON.stringify(json)); }); test('Var delete', function() { @@ -319,7 +314,7 @@ suite('Events', function() { var expectedJson = ({type: "var_delete", varId: "id1", varType: "type1", varName: "name1"}); - assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); + chai.assert.equal(JSON.stringify(expectedJson), JSON.stringify(json)); }); test('Var rename', function() { @@ -328,57 +323,326 @@ suite('Events', function() { var expectedJson = ({type: "var_rename", varId: "id1", oldName: "name1", newName: "name2"}); - assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); + chai.assert.equal(JSON.stringify(expectedJson), JSON.stringify(json)); }); }); - suite.skip('Run Forward', function() { + suite('Run Forward', function() { test('Var create', function() { - var json = {type: "var_create", varId: "id1", varType: "type1", - varName: "name1"}; + var json = {type: "var_create", varId: "id2", varType: "type2", + varName: "name2"}; var event = Blockly.Events.fromJson(json, this.workspace); - assertNull(this.workspace.getVariableById('id1')); + var x = this.workspace.getVariableById('id2'); + chai.assert.isNull(x); event.run(true); - checkVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); test('Var delete', function() { var event = new Blockly.Events.VarDelete(this.variable); - assertNotNull(this.workspace.getVariableById('id1')); + chai.assert.isNotNull(this.workspace.getVariableById('id1')); event.run(true); - assertNull(this.workspace.getVariableById('id1')); + chai.assert.isNull(this.workspace.getVariableById('id1')); }); test('Var rename', function() { var event = new Blockly.Events.VarRename(this.variable, 'name2'); event.run(true); - assertNull(this.workspace.getVariable('name1')); + chai.assert.isNull(this.workspace.getVariable('name1')); checkVariableValues(this.workspace, 'name2', 'type1', 'id1'); }); }); - suite.skip('Run Backward', function() { + suite('Run Backward', function() { test('Var create', function() { - var variable = this.workspace.createVariable('name1', 'type1', 'id1'); - var event = new Blockly.Events.VarCreate(variable); - assertNotNull(this.workspace.getVariableById('id1')); + var event = new Blockly.Events.VarCreate(this.variable); + chai.assert.isNotNull(this.workspace.getVariableById('id1')); event.run(false); }); test('Var delete', function() { - var json = {type: "var_delete", varId: "id1", varType: "type1", - varName: "name1"}; + var json = {type: "var_delete", varId: "id2", varType: "type2", + varName: "name2"}; var event = Blockly.Events.fromJson(json, this.workspace); - assertNull(this.workspace.getVariableById('id1')); + chai.assert.isNull(this.workspace.getVariableById('id2')); event.run(false); - checkVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); test('Var rename', function() { var event = new Blockly.Events.VarRename(this.variable, 'name2'); event.run(false); - assertNull(this.workspace.getVariable('name2')); + chai.assert.isNull(this.workspace.getVariable('name2')); checkVariableValues(this.workspace, 'name1', 'type1', 'id1'); }); }); }); + + suite('Filters', function() { + + function addMoveEvent(events, block, newX, newY) { + events.push(new Blockly.Events.BlockMove(block)); + block.xy_ = new Blockly.utils.Coordinate(newX, newY); + events[events.length - 1].recordNew(); + } + + function addMoveEventParent(events, block, parent) { + events.push(new Blockly.Events.BlockMove(block)); + block.setParent(parent); + events[events.length - 1].recordNew(); + } + + test('No removed, order unchanged', function() { + var block = this.workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.BlockCreate(block), + new Blockly.Events.BlockMove(block), + new Blockly.Events.BlockChange(block, 'field', 'VAR', 'id1', 'id2'), + new Blockly.Events.Ui(block, 'click', undefined, undefined) + ]; + var filteredEvents = Blockly.Events.filter(events, true); + chai.assert.equal(4, filteredEvents.length); // no event should have been removed. + // test that the order hasn't changed + chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); + chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + chai.assert.isTrue(filteredEvents[2] instanceof Blockly.Events.BlockChange); + chai.assert.isTrue(filteredEvents[3] instanceof Blockly.Events.Ui); + }); + + test('Different blocks no removed', function() { + var block1 = this.workspace.newBlock('field_variable_test_block', '1'); + var block2 = this.workspace.newBlock('field_variable_test_block', '2'); + var events = [ + new Blockly.Events.BlockCreate(block1), + new Blockly.Events.BlockMove(block1), + new Blockly.Events.BlockCreate(block2), + new Blockly.Events.BlockMove(block2) + ]; + var filteredEvents = Blockly.Events.filter(events, true); + chai.assert.equal(4, filteredEvents.length); // no event should have been removed. + }); + + test('Forward', function() { + var block = this.workspace.newBlock('field_variable_test_block', '1'); + var events = [ new Blockly.Events.BlockCreate(block) ]; + addMoveEvent(events, block, 1, 1); + addMoveEvent(events, block, 2, 2); + addMoveEvent(events, block, 3, 3); + var filteredEvents = Blockly.Events.filter(events, true); + chai.assert.equal(2, filteredEvents.length); // duplicate moves should have been removed. + // test that the order hasn't changed + chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); + chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + chai.assert.equal(3, filteredEvents[1].newCoordinate.x); + chai.assert.equal(3, filteredEvents[1].newCoordinate.y); + }); + + test('Backward', function() { + var block = this.workspace.newBlock('field_variable_test_block', '1'); + var events = [ new Blockly.Events.BlockCreate(block) ]; + addMoveEvent(events, block, 1, 1); + addMoveEvent(events, block, 2, 2); + addMoveEvent(events, block, 3, 3); + var filteredEvents = Blockly.Events.filter(events, false); + chai.assert.equal(2, filteredEvents.length); // duplicate event should have been removed. + // test that the order hasn't changed + chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockCreate); + chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + chai.assert.equal(1, filteredEvents[1].newCoordinate.x); + chai.assert.equal(1, filteredEvents[1].newCoordinate.y); + }); + + test('Merge move events', function() { + var block = this.workspace.newBlock('field_variable_test_block', '1'); + var events = []; + addMoveEvent(events, block, 0, 0); + addMoveEvent(events, block, 1, 1); + var filteredEvents = Blockly.Events.filter(events, true); + chai.assert.equal(1, filteredEvents.length); // second move event merged into first + chai.assert.equal(1, filteredEvents[0].newCoordinate.x); + chai.assert.equal(1, filteredEvents[0].newCoordinate.y); + }); + + test('Merge change events', function() { + var block1 = this.workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.Change(block1, 'field', 'VAR', 'item', 'item1'), + new Blockly.Events.Change(block1, 'field', 'VAR', 'item1', 'item2') + ]; + var filteredEvents = Blockly.Events.filter(events, true); + chai.assert.equal(1, filteredEvents.length); // second change event merged into first + chai.assert.equal(filteredEvents[0].oldValue, 'item'); + chai.assert.equal(filteredEvents[0].newValue, 'item2'); + }); + + test('Merge ui events', function() { + var block1 = this.workspace.newBlock('field_variable_test_block', '1'); + var block2 = this.workspace.newBlock('field_variable_test_block', '2'); + var block3 = this.workspace.newBlock('field_variable_test_block', '3'); + var events = [ + new Blockly.Events.Ui(block1, 'commentOpen', 'false', 'true'), + new Blockly.Events.Ui(block1, 'click', 'false', 'true'), + new Blockly.Events.Ui(block2, 'mutatorOpen', 'false', 'true'), + new Blockly.Events.Ui(block2, 'click', 'false', 'true'), + new Blockly.Events.Ui(block3, 'warningOpen', 'false', 'true'), + new Blockly.Events.Ui(block3, 'click', 'false', 'true') + ]; + var filteredEvents = Blockly.Events.filter(events, true); + // click event merged into corresponding *Open event + chai.assert.equal(filteredEvents.length, 3); + chai.assert.equal(filteredEvents[0].element, 'commentOpen'); + chai.assert.equal(filteredEvents[1].element, 'mutatorOpen'); + chai.assert.equal(filteredEvents[2].element, 'warningOpen'); + }); + + test('Colliding events not dropped', function() { + // Tests that events that collide on a (event, block, workspace) tuple + // but cannot be merged do not get dropped during filtering. + var block = this.workspace.newBlock('field_variable_test_block', '1'); + var events = [ + new Blockly.Events.Ui(block, 'click', undefined, undefined), + new Blockly.Events.Ui(block, 'stackclick', undefined, undefined) + ]; + var filteredEvents = Blockly.Events.filter(events, true); + // click and stackclick should both exist + chai.assert.equal(2, filteredEvents.length); + chai.assert.equal(filteredEvents[0].element, 'click'); + chai.assert.equal(filteredEvents[1].element, 'stackclick'); + }); + + test('Merging null operations dropped', function() { + // Mutator composition could result in move events for blocks + // connected to the mutated block that were null operations. This + // leads to events in the undo/redo queue that do nothing, requiring + // an extra undo/redo to proceed to the next event. This test ensures + // that two move events that do get merged (disconnecting and + // reconnecting a block in response to a mutator change) are filtered + // from the queue. + var block = this.workspace.newBlock('field_variable_test_block', '1'); + block.setParent(null); + var events = []; + addMoveEventParent(events, block, null); + addMoveEventParent(events, block, null); + var filteredEvents = Blockly.Events.filter(events, true); + // The two events should be merged, but because nothing has changed + // they will be filtered out. + chai.assert.equal(0, filteredEvents.length); + }); + + test('Move events different blocks not merged', function() { + // Move events should only merge if they refer to the same block and are + // consecutive. + // See github.com/google/blockly/pull/1892 for a worked example showing + // how merging non-consecutive events can fail when replacing a shadow + // block. + var block1 = createSimpleTestBlock(this.workspace); + var block2 = createSimpleTestBlock(this.workspace); + + var events = []; + addMoveEvent(events, block1, 1, 1); + addMoveEvent(events, block2, 1, 1); + events.push(new Blockly.Events.BlockDelete(block2)); + addMoveEvent(events, block1, 2, 2); + + var filteredEvents = Blockly.Events.filter(events, true); + // Nothing should have merged. + chai.assert.equal(4, filteredEvents.length); + // test that the order hasn't changed + chai.assert.isTrue(filteredEvents[0] instanceof Blockly.Events.BlockMove); + chai.assert.isTrue(filteredEvents[1] instanceof Blockly.Events.BlockMove); + chai.assert.isTrue(filteredEvents[2] instanceof Blockly.Events.BlockDelete); + chai.assert.isTrue(filteredEvents[3] instanceof Blockly.Events.BlockMove); + }); + }); + + suite('Firing', function() { + setup(function() { + createEventsFireStub(); + }); + + teardown(function() { + sinon.restore(); + }); + + test('Block dispose triggers BlockDelete', function() { + try { + var toolbox = document.getElementById('toolbox-categories'); + var workspaceSvg = Blockly.inject('blocklyDiv', {toolbox: toolbox}); + Blockly.Events.fire.firedEvents_ = []; + + var block = workspaceSvg.newBlock(''); + block.initSvg(); + block.setCommentText('test comment'); + + var event = new Blockly.Events.BlockDelete(block); + + workspaceSvg.clearUndo(); + block.dispose(); + + var firedEvents = workspaceSvg.undoStack_; + chai.assert.equal( + Blockly.Xml.domToText(firedEvents[0].oldXml), + Blockly.Xml.domToText(event.oldXml), + 'Delete event created by dispose'); + } finally { + workspaceSvg.dispose(); + } + }); + + test('New block new var', function() { + // Expect three calls to genUid: one to set the block's ID, one for the event + // group's id, and one for the variable's ID. + var stub = sinon.stub(Blockly.utils, "genUid"); + stub.onCall(0).returns('1'); + stub.onCall(1).returns('2'); + stub.onCall(2).returns('3'); + var _ = this.workspace.newBlock('field_variable_test_block'); + + var firedEvents = this.workspace.undoStack_; + // Expect two events: varCreate and block create. + chai.assert.equal(2, firedEvents.length); + + var event0 = firedEvents[0]; + var event1 = firedEvents[1]; + chai.assert.equal(event0.type, 'var_create'); + chai.assert.equal(event1.type, 'create'); + + // Expect the events to have the same group ID. + chai.assert.equal(event0.group, event1.group); + + // Expect the group ID to be the result of the second call to genUid. + chai.assert.equal(event0.group, '2'); + + // Expect the workspace to have a variable with ID '3'. + chai.assert.isNotNull(this.workspace.getVariableById('3')); + chai.assert.equal(event0.varId, '3'); + }); + + test('New block new var xml', function() { + // The sequence of events should be the same whether the block was created from + // XML or directly. + var dom = Blockly.Xml.textToDom( + '' + + ' ' + + ' name1' + + ' ' + + ''); + Blockly.Xml.domToWorkspace(dom, this.workspace); + + var firedEvents = this.workspace.undoStack_; + // Expect two events: varCreate and block create. + chai.assert.equal(2, firedEvents.length); + + var event0 = firedEvents[0]; + var event1 = firedEvents[1]; + chai.assert.equal(event0.type, 'var_create'); + chai.assert.equal(event1.type, 'create'); + + // Expect the events to have the same group ID. + chai.assert.equal(event0.group, event1.group); + + // Expect the workspace to have a variable with ID 'id1'. + chai.assert.isNotNull(this.workspace.getVariableById('id1')); + chai.assert.equal(event0.varId, 'id1'); + }); + }); }); diff --git a/tests/mocha/extensions_test.js b/tests/mocha/extensions_test.js new file mode 100644 index 000000000..ea9a81bb4 --- /dev/null +++ b/tests/mocha/extensions_test.js @@ -0,0 +1,511 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +suite('Extensions', function() { + setup(function() { + this.workspace = new Blockly.Workspace(); + this.blockTypesCleanup_ = []; + this.extensionsCleanup_ = []; + }); + teardown(function() { + var i; + for (i = 0; i < this.blockTypesCleanup_.length; i++) { + var blockType = this.blockTypesCleanup_[i]; + delete Blockly.Blocks[blockType]; + } + for (i = 0; i < this.extensionsCleanup_.length; i++) { + var extension = this.extensionsCleanup_[i]; + delete Blockly.Extensions.ALL_[extension]; + } + this.workspace.dispose(); + // Clear Blockly.Event state. + Blockly.Events.setGroup(false); + Blockly.Events.disabled_ = 0; + sinon.restore(); + }); + + test('Definition before and after block type', function() { + this.extensionsCleanup_.push('extensions_test_before'); + this.extensionsCleanup_.push('extensions_test_after'); + this.blockTypesCleanup_.push('extension_test_block'); + + chai.assert.isUndefined(Blockly.Extensions.ALL_['extensions_test_before']); + var beforeCallback = sinon.spy(); + // Extension defined before the block type is defined. + Blockly.Extensions.register('extensions_test_before', beforeCallback); + + Blockly.defineBlocksWithJsonArray([{ + "type": "extension_test_block", + "message0": "extension_test_block", + "extensions": ["extensions_test_before", "extensions_test_after"] + }]); + + chai.assert.isUndefined(Blockly.Extensions.ALL_['extensions_test_after']); + var afterCallback = sinon.spy(); + // Extension defined after the block type (but before instantiation). + Blockly.Extensions.register('extensions_test_after', afterCallback); + + chai.assert.typeOf(Blockly.Extensions.ALL_['extensions_test_before'], 'function'); + chai.assert.typeOf(Blockly.Extensions.ALL_['extensions_test_after'], 'function'); + sinon.assert.notCalled(beforeCallback); + sinon.assert.notCalled(afterCallback); + + var block = new Blockly.Block(this.workspace, 'extension_test_block'); + + sinon.assert.calledOnce(beforeCallback); + sinon.assert.calledOnce(afterCallback); + sinon.assert.calledOn(beforeCallback, block); + sinon.assert.calledOn(afterCallback, block); + }); + + test('Parent tooltip when inline', function() { + this.blockTypesCleanup_.push('test_parent_tooltip_when_inline'); + this.blockTypesCleanup_.push('test_parent'); + + var defaultTooltip = "defaultTooltip"; + var parentTooltip = "parentTooltip"; + Blockly.defineBlocksWithJsonArray([ + { + "type": "test_parent_tooltip_when_inline", + "message0": "test_parent_tooltip_when_inline", + "output": true, + "tooltip": defaultTooltip, + "extensions": ["parent_tooltip_when_inline"] + }, + { + "type": "test_parent", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "tooltip": parentTooltip + } + ]); + + var block = + new Blockly.Block(this.workspace, 'test_parent_tooltip_when_inline'); + + // Tooltip is dynamic after extension initialization. + chai.assert.typeOf(block.tooltip, 'function'); + chai.assert.equal(block.tooltip(), defaultTooltip); + + // Tooltip is normal before connected to parent. + var parent = new Blockly.Block(this.workspace, 'test_parent'); + chai.assert.equal(parent.tooltip, parentTooltip); + chai.assert.isFalse(!!parent.inputsInline); + + // Tooltip is normal when parent is not inline. + parent.getInput('INPUT').connection.connect(block.outputConnection); + chai.assert.equal(block.getParent(), parent); + chai.assert.equal(block.tooltip(), defaultTooltip); + + // Tooltip is parent's when parent is inline. + parent.setInputsInline(true); + chai.assert.equal(block.tooltip(), parentTooltip); + + // Tooltip revert when disconnected. + parent.getInput('INPUT').connection.disconnect(); + chai.assert.notExists(block.getParent()); + chai.assert.equal(block.tooltip(), defaultTooltip); + }); + + suite('Mixin', function() { + test('Basic', function() { + this.extensionsCleanup_.push('mixin_test'); + this.blockTypesCleanup_.push('test_block_mixin'); + + var testMixin = { + field: 'FIELD', + method: function() { + console.log('TEXT_MIXIN method()'); + } + }; + + chai.assert.isUndefined(Blockly.Extensions.ALL_['mixin_test']); + // Extension defined before the block type is defined. + Blockly.Extensions.registerMixin('mixin_test', testMixin); + + chai.assert.typeOf(Blockly.Extensions.ALL_['mixin_test'], 'function'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "test_block_mixin", + "message0": "test_block_mixin", + "extensions": ["mixin_test"] + }]); + + var block = new Blockly.Block(this.workspace, 'test_block_mixin'); + + chai.assert.equal(testMixin.field, block.field); + chai.assert.equal(testMixin.method, block.method); + }); + + suite('Mutator', function() { + test('Basic', function() { + this.extensionsCleanup_.push('mutator_test'); + this.blockTypesCleanup_.push('mutator_test_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "mutator_test_block", + "message0": "mutator_test_block", + "mutator": "mutator_test" + }]); + + // Events code calls mutationToDom and expects it to give back a meaningful + // value. + Blockly.Events.disable(); + Blockly.Extensions.registerMutator('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + }, + compose: function() { + return 'composeFn'; + }, + decompose: function() { + return 'decomposeFn'; + } + }); + + var block = new Blockly.Block(this.workspace, 'mutator_test_block'); + + // Make sure all of the functions were installed correctly. + chai.assert.equal(block.domToMutation(), 'domToMutationFn'); + chai.assert.equal(block.mutationToDom(), 'mutationToDomFn'); + chai.assert.equal(block.compose(), 'composeFn'); + chai.assert.equal(block.decompose(), 'decomposeFn'); + }); + + test('With helper function', function() { + this.extensionsCleanup_.push('extensions_test'); + this.blockTypesCleanup_.push('mutator_test_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "mutator_test_block", + "message0": "mutator_test_block", + "mutator": ["extensions_test"] + }]); + + // Events code calls mutationToDom and expects it to give back a + // meaningful value. + Blockly.Events.disable(); + chai.assert.isUndefined(Blockly.Extensions.ALL_['extensions_test']); + var helperFunctionSpy = sinon.spy(); + Blockly.Extensions.registerMutator('extensions_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + } + }, + helperFunctionSpy + ); + + var _ = new Blockly.Block(this.workspace, 'mutator_test_block'); + + sinon.assert.calledOnce(helperFunctionSpy); + }); + + test('No dialog', function() { + this.extensionsCleanup_.push('mutator_test'); + this.blockTypesCleanup_.push('mutator_test_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "mutator_test_block", + "message0": "mutator_test_block", + "mutator": "mutator_test" + }]); + + // Events code calls mutationToDom and expects it to give back a + // meaningful value. + Blockly.Events.disable(); + chai.assert.isUndefined(Blockly.Extensions.ALL_['mutator_test']); + Blockly.Extensions.registerMutator('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + } + }); + + var block = new Blockly.Block(this.workspace, 'mutator_test_block'); + + // Make sure all of the functions were installed correctly. + chai.assert.equal(block.domToMutation(), 'domToMutationFn'); + chai.assert.equal(block.mutationToDom(), 'mutationToDomFn'); + chai.assert.isFalse(block.hasOwnProperty('compose')); + chai.assert.isFalse(block.hasOwnProperty('decompose')); + }); + }); + }); + + suite('Error cases', function() { + test('Missing extension', function() { + this.blockTypesCleanup_.push('missing_extension_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "missing_extension_block", + "message0": "missing_extension_block", + "extensions": ["missing_extension"] + }]); + + chai.assert.isUndefined(Blockly.Extensions.ALL_['missing_extension']); + var workspace = this.workspace; + chai.assert.throws(function() { + var _ = new Blockly.Block(workspace, 'missing_extension_block'); + }); + }); + + test('Mixin overwrites local value', function() { + this.extensionsCleanup_.push('mixin_bad_inputList'); + this.blockTypesCleanup_.push('test_block_bad_inputList'); + + var TEST_MIXIN_BAD_INPUTLIST = { + inputList: 'bad inputList' // Defined in constructor + }; + + chai.assert.isUndefined(Blockly.Extensions.ALL_['mixin_bad_inputList']); + // Extension defined before the block type is defined. + Blockly.Extensions.registerMixin('mixin_bad_inputList', TEST_MIXIN_BAD_INPUTLIST); + chai.assert.typeOf(Blockly.Extensions.ALL_['mixin_bad_inputList'], 'function'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "test_block_bad_inputList", + "message0": "test_block_bad_inputList", + "extensions": ["mixin_bad_inputList"] + }]); + + var workspace = this.workspace; + chai.assert.throws(function() { + var _ = new Blockly.Block(workspace, 'test_block_bad_inputList'); + }, /inputList/); + }); + + test('Mixin overwrites prototype', function() { + this.extensionsCleanup_.push('mixin_bad_colour_'); + this.blockTypesCleanup_.push('test_block_bad_colour'); + + var TEST_MIXIN_BAD_COLOUR = { + colour_: 'bad colour_' // Defined on prototype + }; + + chai.assert.isUndefined(Blockly.Extensions.ALL_['mixin_bad_colour_']); + // Extension defined before the block type is defined. + Blockly.Extensions.registerMixin('mixin_bad_colour_', TEST_MIXIN_BAD_COLOUR); + chai.assert.typeOf(Blockly.Extensions.ALL_['mixin_bad_colour_'], 'function'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "test_block_bad_colour", + "message0": "test_block_bad_colour", + "extensions": ["mixin_bad_colour_"] + }]); + + var workspace = this.workspace; + chai.assert.throws(function() { + var _ = new Blockly.Block(workspace, 'test_block_bad_colour'); + }, /colour_/); + }); + + test('Use mutator as extension', function() { + this.extensionsCleanup_.push('mutator_test'); + this.blockTypesCleanup_.push('mutator_test_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "mutator_test_block", + "message0": "mutator_test_block", + "extensions": ["mutator_test"] + }]); + + // Events code calls mutationToDom and expects it to give back a + // meaningful value. + Blockly.Events.disable(); + chai.assert.isUndefined(Blockly.Extensions.ALL_['mutator_test']); + Blockly.Extensions.registerMutator('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + } + }); + + var workspace = this.workspace; + chai.assert.throws(function() { + var _ = new Blockly.Block(workspace, 'mutator_test_block'); + }); + // Should have failed on apply, not on register. + chai.assert.isNotNull(Blockly.Extensions.ALL_['mutator_test']); + }); + + test('Use mutator mixin as extension', function() { + this.extensionsCleanup_.push('mutator_test'); + this.blockTypesCleanup_.push('mutator_test_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "mutator_test_block", + "message0": "mutator_test_block", + "extensions": ["mutator_test"] + }]); + + // Events code calls mutationToDom and expects it to give back a + // meaningful value. + Blockly.Events.disable(); + chai.assert.isUndefined(Blockly.Extensions.ALL_['mutator_test']); + Blockly.Extensions.registerMixin('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + } + }); + + var workspace = this.workspace; + chai.assert.throws(function() { + var _ = new Blockly.Block(workspace, 'mutator_test_block'); + }); + // Should have failed on apply, not on register. + chai.assert.isNotNull(Blockly.Extensions.ALL_['mutator_test']); + }); + + test('Use extension as mutator', function() { + this.extensionsCleanup_.push('extensions_test'); + this.blockTypesCleanup_.push('mutator_test_block'); + + Blockly.defineBlocksWithJsonArray([{ + "type": "mutator_test_block", + "message0": "mutator_test_block", + "mutator": ["extensions_test"] + }]); + + // Events code calls mutationToDom and expects it to give back a + // meaningful value. + Blockly.Events.disable(); + chai.assert.isUndefined(Blockly.Extensions.ALL_['extensions_test']); + Blockly.Extensions.register('extensions_test', function() { + return 'extensions_test_fn'; + }); + + var workspace = this.workspace; + chai.assert.throws(function() { + var _ = new Blockly.Block(workspace, 'mutator_test_block'); + }); + // Should have failed on apply, not on register. + chai.assert.isNotNull(Blockly.Extensions.ALL_['extensions_test']); + }); + + suite('register', function() { + test('Just a string', function() { + this.extensionsCleanup_.push('extension_just_a_string'); + chai.assert.isUndefined(Blockly.Extensions.ALL_['extension_just_a_string']); + chai.assert.throws(function() { + Blockly.Extensions.register('extension_just_a_string', null); + }); + }); + + test('Null', function() { + this.extensionsCleanup_.push('extension_is_null'); + chai.assert.isUndefined(Blockly.Extensions.ALL_['extension_is_null']); + chai.assert.throws(function() { + Blockly.Extensions.register('extension_is_null', null); + }); + }); + + test('Undefined', function() { + this.extensionsCleanup_.push('extension_is_undefined'); + chai.assert.isUndefined(Blockly.Extensions.ALL_['extension_is_undefined']); + chai.assert.throws(function() { + Blockly.Extensions.register('extension_is_undefined', null); + }); + }); + }); + + suite('registerMutator', function() { + test('No domToMutation', function() { + this.extensionsCleanup_.push('mutator_test'); + chai.assert.throws(function() { + Blockly.Extensions.registerMutator('mutator_test', + { + mutationToDom: function() { + return 'mutationToDomFn'; + }, + compose: function() { + return 'composeFn'; + }, + decompose: function() { + return 'decomposeFn'; + } + }); + }, /domToMutation/); + }); + + test('No mutationToDom', function() { + this.extensionsCleanup_.push('mutator_test'); + chai.assert.throws(function() { + Blockly.Extensions.registerMutator('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + compose: function() { + return 'composeFn'; + }, + decompose: function() { + return 'decomposeFn'; + } + }); + }, /mutationToDom/); + }); + + test('Has decompose but no compose', function() { + this.extensionsCleanup_.push('mutator_test'); + chai.assert.throws(function() { + Blockly.Extensions.registerMutator('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + }, + decompose: function() { + return 'decomposeFn'; + } + }); + }, /compose/); + }); + + test('Has compose but no decompose', function() { + this.extensionsCleanup_.push('mutator_test'); + chai.assert.throws(function() { + Blockly.Extensions.registerMutator('mutator_test', + { + domToMutation: function() { + return 'domToMutationFn'; + }, + mutationToDom: function() { + return 'mutationToDomFn'; + }, + compose: function() { + return 'composeFn'; + } + }); + }, /decompose/); + }); + }); + }); +}); diff --git a/tests/mocha/field_angle_test.js b/tests/mocha/field_angle_test.js index 6bbc838a4..74206c888 100644 --- a/tests/mocha/field_angle_test.js +++ b/tests/mocha/field_angle_test.js @@ -5,250 +5,139 @@ */ suite('Angle Fields', function() { - function assertValue(angleField, expectedValue, opt_expectedText) { - var actualValue = angleField.getValue(); - var actualText = angleField.getText(); - opt_expectedText = opt_expectedText || String(expectedValue); - assertEquals(String(actualValue), String(expectedValue)); - assertEquals(Number(actualValue), expectedValue); - assertEquals(actualText, opt_expectedText); - } - function assertValueDefault(angleField) { - assertValue(angleField, 0); - } - suite('Constructor', function() { - test('Empty', function() { - var angleField = new Blockly.FieldAngle(); - assertValueDefault(angleField); - }); - test('Undefined', function() { - var angleField = new Blockly.FieldAngle(undefined); - assertValueDefault(angleField); - }); - test('NaN', function() { - var angleField = new Blockly.FieldAngle(NaN); - assertValueDefault(angleField); - }); - test('Integer', function() { - var angleField = new Blockly.FieldAngle(1); - assertValue(angleField, 1); - }); - test('Float', function() { - var angleField = new Blockly.FieldAngle(1.5); - assertValue(angleField, 1.5); - }); - test('Integer String', function() { - var angleField = new Blockly.FieldAngle('1'); - assertValue(angleField, 1); - }); - test('Float String', function() { - var angleField = new Blockly.FieldAngle('1.5'); - assertValue(angleField, 1.5); - }); - test('> 360°', function() { - var angleField = new Blockly.FieldAngle(362); - assertValue(angleField, 2); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var angleField = Blockly.FieldAngle.fromJson({}); - assertValueDefault(angleField); - }); - test('Undefined', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:undefined }); - assertValueDefault(angleField); - }); - test('NaN', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:NaN }); - assertValueDefault(angleField); - }); - test('Integer', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:1 }); - assertValue(angleField, 1); - }); - test('Float', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:1.5 }); - assertValue(angleField, 1.5); - }); - test('Integer String', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:'1' }); - assertValue(angleField, 1); - }); - test('Float String', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:'1.5' }); - assertValue(angleField, 1.5); - }); - test('> 360°', function() { - var angleField = Blockly.FieldAngle.fromJson({ angle:362 }); - assertValue(angleField, 2); - }); - }); + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + {title: 'NaN', value: NaN}, + {title: 'Non-Parsable String', value: 'bad'}, + {title: 'Infinity', value: Infinity, expectedValue: Infinity}, + {title: 'Negative Infinity', value: -Infinity, expectedValue: -Infinity}, + {title: 'Infinity String', value: 'Infinity', expectedValue: Infinity}, + {title: 'Negative Infinity String', value: '-Infinity', + expectedValue: -Infinity}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + + var validValueTestCases = [ + {title: 'Integer', value: 1, expectedValue: 1}, + {title: 'Float', value: 1.5, expectedValue: 1.5}, + {title: 'Integer String', value: '1', expectedValue: 1}, + {title: 'Float String', value: '1.5', expectedValue: 1.5}, + {title: '> 360°', value: 362, expectedValue: 2}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = [testCase.value]; + testCase.json = {'angle': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = 0; + /** + * Asserts that the field property values are set to default. + * @param {FieldTemplate} field The field to check. + */ + var assertFieldDefault = function(field) { + testHelpers.assertFieldValue(field, defaultFieldValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldAngle} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + testHelpers.assertFieldValue(field, testCase.expectedValue); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldAngle, validValueTestCases, invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldAngle, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { suite('Empty -> New Value', function() { setup(function() { - this.angleField = new Blockly.FieldAngle(); + this.field = new Blockly.FieldAngle(); }); - test('Null', function() { - this.angleField.setValue(null); - assertValueDefault(this.angleField); - }); - test('Undefined', function() { - this.angleField.setValue(undefined); - assertValueDefault(this.angleField); - }); - test('Non-Parsable String', function() { - this.angleField.setValue('bad'); - assertValueDefault(this.angleField); - }); - test('NaN', function() { - this.angleField.setValue(NaN); - assertValueDefault(this.angleField); - }); - test('Integer', function() { - this.angleField.setValue(2); - assertValue(this.angleField, 2); - }); - test('Float', function() { - this.angleField.setValue(2.5); - assertValue(this.angleField, 2.5); - }); - test('Integer String', function() { - this.angleField.setValue('2'); - assertValue(this.angleField, 2); - }); - test('Float', function() { - this.angleField.setValue('2.5'); - assertValue(this.angleField, 2.5); - }); - test('>360°', function() { - this.angleField.setValue(362); - assertValue(this.angleField, 2); - }); - test('Infinity', function() { - this.angleField.setValue(Infinity); - assertValueDefault(this.angleField); - }); - test('Negative Infinity String', function() { - this.angleField.setValue('-Infinity'); - assertValueDefault(this.angleField); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, defaultFieldValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue(2.5); + testHelpers.assertFieldValue(this.field, 2.5); }); }); suite('Value -> New Value', function() { + var initialValue = 1; setup(function() { - this.angleField = new Blockly.FieldAngle(1); + this.field = new Blockly.FieldAngle(initialValue); }); - test('Null', function() { - this.angleField.setValue(null); - assertValue(this.angleField, 1); - }); - test('Undefined', function() { - this.angleField.setValue(undefined); - assertValue(this.angleField, 1); - }); - test('Non-Parsable String', function() { - this.angleField.setValue('bad'); - assertValue(this.angleField, 1); - }); - test('NaN', function() { - this.angleField.setValue(NaN); - assertValue(this.angleField, 1); - }); - test('Integer', function() { - this.angleField.setValue(2); - assertValue(this.angleField, 2); - }); - test('Float', function() { - this.angleField.setValue(2.5); - assertValue(this.angleField, 2.5); - }); - test('Integer String', function() { - this.angleField.setValue('2'); - assertValue(this.angleField, 2); - }); - test('Float', function() { - this.angleField.setValue('2.5'); - assertValue(this.angleField, 2.5); - }); - test('>360°', function() { - this.angleField.setValue(362); - assertValue(this.angleField, 2); - }); - test('Infinity', function() { - this.angleField.setValue(Infinity); - assertValue(this.angleField, 1); - }); - test('Negative Infinity String', function() { - this.angleField.setValue('-Infinity'); - assertValue(this.angleField, 1); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, initialValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue(2.5); + testHelpers.assertFieldValue(this.field, 2.5); }); }); }); suite('Validators', function() { setup(function() { - this.angleField = new Blockly.FieldAngle(1); - this.angleField.htmlInput_ = Object.create(null); - this.angleField.htmlInput_.oldValue_ = '1'; - this.angleField.htmlInput_.untypedDefaultValue_ = 1; - this.stub = sinon.stub(this.angleField, 'resizeEditor_'); + this.field = new Blockly.FieldAngle(1); + this.field.htmlInput_ = Object.create(null); + this.field.htmlInput_.oldValue_ = '1'; + this.field.htmlInput_.untypedDefaultValue_ = 1; + this.stub = sinon.stub(this.field, 'resizeEditor_'); }); teardown(function() { - this.angleField.setValidator(null); - this.angleField.htmlInput_ = null; - if (this.stub) { - this.stub.restore(); - } + sinon.restore(); }); - suite('Null Validator', function() { - setup(function() { - this.angleField.setValidator(function() { - return null; + var testSuites = [ + {title: 'Null Validator', + validator: + function() { + return null; + }, + value: 2, expectedValue: 1}, + {title: 'Force Mult of 30 Validator', + validator: + function(newValue) { + return Math.round(newValue / 30) * 30; + }, + value: 25, expectedValue: 30}, + {title: 'Returns Undefined Validator', validator: function() {}, value: 2, + expectedValue: 2}, + ]; + testSuites.forEach(function(suiteInfo) { + suite(suiteInfo.title, function() { + setup(function() { + this.field.setValidator(suiteInfo.validator); }); - }); - test('When Editing', function() { - this.angleField.isBeingEdited_ = true; - this.angleField.htmlInput_.value = '2'; - this.angleField.onHtmlInputChange_(null); - assertValue(this.angleField, 1, '2'); - this.angleField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.angleField.setValue(2); - assertValue(this.angleField, 1); - }); - }); - suite('Force Mult of 30 Validator', function() { - setup(function() { - this.angleField.setValidator(function(newValue) { - return Math.round(newValue / 30) * 30; + test('When Editing', function() { + this.field.isBeingEdited_ = true; + this.field.htmlInput_.value = String(suiteInfo.value); + this.field.onHtmlInputChange_(null); + testHelpers.assertFieldValue( + this.field, suiteInfo.expectedValue, String(suiteInfo.value)); + }); + test('When Not Editing', function() { + this.field.setValue(suiteInfo.value); + testHelpers.assertFieldValue(this.field, suiteInfo.expectedValue); }); - }); - test('When Editing', function() { - this.angleField.isBeingEdited_ = true; - this.angleField.htmlInput_.value = '25'; - this.angleField.onHtmlInputChange_(null); - assertValue(this.angleField, 30, '25'); - this.angleField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.angleField.setValue(25); - assertValue(this.angleField, 30); - }); - }); - suite('Returns Undefined Validator', function() { - setup(function() { - this.angleField.setValidator(function() {}); - }); - test('When Editing', function() { - this.angleField.isBeingEdited_ = true; - this.angleField.htmlInput_.value = '2'; - this.angleField.onHtmlInputChange_(null); - assertValue(this.angleField, 2); - this.angleField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.angleField.setValue(2); - assertValue(this.angleField, 2); }); }); }); diff --git a/tests/mocha/field_checkbox_test.js b/tests/mocha/field_checkbox_test.js index edfdebee2..7eaac4d5a 100644 --- a/tests/mocha/field_checkbox_test.js +++ b/tests/mocha/field_checkbox_test.js @@ -5,67 +5,68 @@ */ suite('Checkbox Fields', function() { - function assertValue(checkboxField, expectedValue, expectedText) { - var actualValue = checkboxField.getValue(); - var actualText = checkboxField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedText); - } - function assertValueDefault(checkboxField) { - assertValue(checkboxField, 'FALSE', 'false'); - } - suite('Constructor', function() { - test('Empty', function() { - var checkboxField = new Blockly.FieldCheckbox(); - assertValueDefault(checkboxField); - }); - test('Undefined', function() { - var checkboxField = new Blockly.FieldCheckbox(undefined); - assertValueDefault(checkboxField); - }); - test('True', function() { - var checkboxField = new Blockly.FieldCheckbox(true); - assertValue(checkboxField, 'TRUE', 'true'); - }); - test('False', function() { - var checkboxField = new Blockly.FieldCheckbox(false); - assertValue(checkboxField, 'FALSE', 'false'); - }); - test('String TRUE', function() { - var checkboxField = new Blockly.FieldCheckbox('TRUE'); - assertValue(checkboxField, 'TRUE', 'true'); - }); - test('String FALSE', function() { - var checkboxField = new Blockly.FieldCheckbox('FALSE'); - assertValue(checkboxField, 'FALSE', 'false'); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var checkboxField = Blockly.FieldCheckbox.fromJson({}); - assertValueDefault(checkboxField); - }); - test('Undefined', function() { - var checkboxField = Blockly.FieldCheckbox.fromJson({ checked: undefined}); - assertValueDefault(checkboxField); - }); - test('True', function() { - var checkboxField = Blockly.FieldCheckbox.fromJson({ checked: true}); - assertValue(checkboxField, 'TRUE', 'true'); - }); - test('False', function() { - var checkboxField = Blockly.FieldCheckbox.fromJson({ checked: false}); - assertValue(checkboxField, 'FALSE', 'false'); - }); - test('String TRUE', function() { - var checkboxField = Blockly.FieldCheckbox.fromJson({ checked: 'TRUE'}); - assertValue(checkboxField, 'TRUE', 'true'); - }); - test('String FALSE', function() { - var checkboxField = Blockly.FieldCheckbox.fromJson({ checked: 'FALSE'}); - assertValue(checkboxField, 'FALSE', 'false'); - }); - }); + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + {title: 'NaN', value: NaN}, + {title: 'Non-Parsable String', value: 'bad'}, + {title: 'Integer', value: 1}, + {title: 'Float', value: 1.5}, + {title: 'String true', value: 'true'}, + {title: 'String false', value: 'false'}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + var validValueTestCases = [ + {title: 'Boolean true', value: true, expectedValue: 'TRUE'}, + {title: 'Boolean false', value: false, expectedValue: 'FALSE'}, + {title: 'String TRUE', value: 'TRUE', expectedValue: 'TRUE'}, + {title: 'String FALSE', value: 'FALSE', expectedValue: 'FALSE'}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = [testCase.value]; + testCase.json = {'checked': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = 'FALSE'; + /** + * Asserts that the field property values are set to default. + * @param {!Blockly.FieldNumber} field The field to check. + */ + var assertFieldDefault = function(field) { + testHelpers.assertFieldValue( + field, defaultFieldValue, defaultFieldValue.toLowerCase()); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldNumber} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + testHelpers.assertFieldValue( + field, testCase.expectedValue, testCase.expectedValue.toLowerCase()); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldCheckbox, validValueTestCases, invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldCheckbox, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { suite('True -> New Value', function() { setup(function() { @@ -73,19 +74,24 @@ suite('Checkbox Fields', function() { }); test('Null', function() { this.checkboxField.setValue(null); - assertValue(this.checkboxField, 'TRUE', 'true'); + testHelpers.assertFieldValue(this.checkboxField, 'TRUE', 'true'); }); test('Undefined', function() { this.checkboxField.setValue(undefined); - assertValue(this.checkboxField, 'TRUE', 'true'); + testHelpers.assertFieldValue(this.checkboxField, 'TRUE', 'true'); }); test('Non-Parsable String', function() { this.checkboxField.setValue('bad'); - assertValue(this.checkboxField, 'TRUE', 'true'); + testHelpers.assertFieldValue(this.checkboxField, 'TRUE', 'true'); }); test('False', function() { this.checkboxField.setValue('FALSE'); - assertValue(this.checkboxField, 'FALSE', 'false'); + testHelpers.assertFieldValue(this.checkboxField, 'FALSE', 'false'); + }); + test('With source block', function() { + this.checkboxField.setSourceBlock(createTestBlock()); + this.checkboxField.setValue('FALSE'); + testHelpers.assertFieldValue(this.checkboxField, 'FALSE', 'false'); }); }); suite('False -> New Value', function() { @@ -94,70 +100,60 @@ suite('Checkbox Fields', function() { }); test('Null', function() { this.checkboxField.setValue(null); - assertValue(this.checkboxField, 'FALSE', 'false'); + testHelpers.assertFieldValue(this.checkboxField, 'FALSE', 'false'); }); test('Undefined', function() { this.checkboxField.setValue(undefined); - assertValue(this.checkboxField, 'FALSE', 'false'); + testHelpers.assertFieldValue(this.checkboxField, 'FALSE', 'false'); }); test('Non-Parsable String', function() { this.checkboxField.setValue('bad'); - assertValue(this.checkboxField, 'FALSE', 'false'); + testHelpers.assertFieldValue(this.checkboxField, 'FALSE', 'false'); }); test('True', function() { this.checkboxField.setValue('TRUE'); - assertValue(this.checkboxField, 'TRUE', 'true'); + testHelpers.assertFieldValue(this.checkboxField, 'TRUE', 'true'); }); }); }); suite('Validators', function() { setup(function() { - this.checkboxField = new Blockly.FieldCheckbox(true); + this.field = new Blockly.FieldCheckbox(true); }); - teardown(function() { - this.checkboxField.setValidator(null); - }); - suite('Null Validator', function() { - setup(function() { - this.checkboxField.setValidator(function() { - return null; + var testSuites = [ + {title: 'Null Validator', + validator: + function() { + return null; + }, + value: 'FALSE', expectedValue: 'TRUE'}, + {title: 'Always True Validator', + validator: + function() { + return 'TRUE'; + }, + value: 'FALSE', expectedValue: 'TRUE'}, + {title: 'Always False Validator', + validator: + function() { + return 'TRUE'; + }, + value: 'FALSE', expectedValue: 'TRUE'}, + {title: 'Returns Undefined Validator', validator: function() {}, + value: 'FALSE', expectedValue: 'FALSE'}, + ]; + testSuites.forEach(function(suiteInfo) { + suite(suiteInfo.title, function() { + setup(function() { + this.field.setValidator(suiteInfo.validator); }); - }); - test('New Value', function() { - this.checkboxField.setValue('FALSE'); - assertValue(this.checkboxField, 'TRUE', 'true'); - }); - }); - suite('Always True Validator', function() { - setup(function() { - this.checkboxField.setValidator(function() { - return 'TRUE'; + test('New Value', function() { + this.field.setValue(suiteInfo.value); + testHelpers.assertFieldValue( + this.field, suiteInfo.expectedValue, + String(suiteInfo.expectedValue).toLowerCase()); }); }); - test('New Value', function() { - this.checkboxField.setValue('FALSE'); - assertValue(this.checkboxField, 'TRUE', 'true'); - }); - }); - suite('Always False Validator', function() { - setup(function() { - this.checkboxField.setValidator(function() { - return 'FALSE'; - }); - }); - test('New Value', function() { - this.checkboxField.setValue('TRUE'); - assertValue(this.checkboxField, 'FALSE', 'false'); - }); - }); - suite('Returns Undefined Validator', function() { - setup(function() { - this.checkboxField.setValidator(function() {}); - }); - test('New Value', function() { - this.checkboxField.setValue('FALSE'); - assertValue(this.checkboxField, 'FALSE', 'false'); - }); }); }); suite('Customizations', function() { diff --git a/tests/mocha/field_colour_test.js b/tests/mocha/field_colour_test.js index 16878c47e..71d1d352a 100644 --- a/tests/mocha/field_colour_test.js +++ b/tests/mocha/field_colour_test.js @@ -5,132 +5,97 @@ */ suite('Colour Fields', function() { - function assertValue(colourField, expectedValue, expectedText) { - var actualValue = colourField.getValue(); - var actualText = colourField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedText); - } - function assertValueDefault(colourField) { - var expectedValue = Blockly.FieldColour.COLOURS[0]; - var expectedText = expectedValue; - var m = expectedValue.match(/^#(.)\1(.)\2(.)\3$/); - if (m) { - expectedText = '#' + m[1] + m[2] + m[3]; - } - assertValue(colourField, expectedValue, expectedText); - } + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + {title: 'NaN', value: NaN}, + {title: 'Non-Parsable String', value: 'bad-string'}, + {title: 'Integer', value: 1}, + {title: 'Float', value: 1.5}, + {title: 'Infinity', value: Infinity, expectedValue: Infinity}, + {title: 'Negative Infinity', value: -Infinity, expectedValue: -Infinity}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ - setup(function() { - this.previousColours = Blockly.FieldColour.COLOURS; - Blockly.FieldColour.Colours = [ - '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#ffffff' - ]; - }); - teardown(function() { - Blockly.FieldColour.Colours = this.previousColours; - }); - suite('Constructor', function() { - test('Empty', function() { - var colourField = new Blockly.FieldColour(); - assertValueDefault(colourField); - }); - test('Undefined', function() { - var colourField = new Blockly.FieldColour(undefined); - assertValueDefault(colourField); - }); - test('#AAAAAA', function() { - var colourField = new Blockly.FieldColour('#AAAAAA'); - assertValue(colourField, '#aaaaaa', '#aaa'); - }); - test('#aaaaaa', function() { - var colourField = new Blockly.FieldColour('#aaaaaa'); - assertValue(colourField, '#aaaaaa', '#aaa'); - }); - test('#AAAA00', function() { - var colourField = new Blockly.FieldColour('#AAAA00'); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('#aaaa00', function() { - var colourField = new Blockly.FieldColour('#aaaa00'); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('#BCBCBC', function() { - var colourField = new Blockly.FieldColour('#BCBCBC'); - assertValue(colourField, '#bcbcbc', '#bcbcbc'); - }); - test('#bcbcbc', function() { - var colourField = new Blockly.FieldColour('#bcbcbc'); - assertValue(colourField, '#bcbcbc', '#bcbcbc'); - }); - test('#AA0', function() { - var colourField = new Blockly.FieldColour('#AA0'); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('#aa0', function() { - var colourField = new Blockly.FieldColour('#aa0'); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('rgb(170, 170, 0)', function() { - var colourField = new Blockly.FieldColour('rgb(170, 170, 0)'); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('red', function() { - var colourField = new Blockly.FieldColour('red'); - assertValue(colourField, '#ff0000', '#f00'); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var colourField = new Blockly.FieldColour.fromJson({}); - assertValueDefault(colourField); - }); - test('Undefined', function() { - var colourField = new Blockly.FieldColour.fromJson({ colour:undefined }); - assertValueDefault(colourField); - }); - test('#AAAAAA', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#AAAAAA' }); - assertValue(colourField, '#aaaaaa', '#aaa'); - }); - test('#aaaaaa', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#aaaaaa' }); - assertValue(colourField, '#aaaaaa', '#aaa'); - }); - test('#AAAA00', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#AAAA00' }); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('#aaaa00', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#aaaa00' }); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('#BCBCBC', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#BCBCBC' }); - assertValue(colourField, '#bcbcbc', '#bcbcbc'); - }); - test('#bcbcbc', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#bcbcbc' }); - assertValue(colourField, '#bcbcbc', '#bcbcbc'); - }); - test('#AA0', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#AA0' }); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('#aa0', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: '#aa0' }); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('rgb(170, 170, 0)', function() { - var colourField = Blockly.FieldColour.fromJson( - { colour: 'rgb(170, 170, 0)' }); - assertValue(colourField, '#aaaa00', '#aa0'); - }); - test('red', function() { - var colourField = Blockly.FieldColour.fromJson({ colour: 'red' }); - assertValue(colourField, '#ff0000', '#f00'); - }); - }); + var validValueTestCases = [ + {title: '#AAAAAA', value: '#AAAAAA', expectedValue: '#aaaaaa', + expectedText: '#aaa'}, + {title: '#aaaaaa', value: '#aaaaaa', expectedValue: '#aaaaaa', + expectedText: '#aaa'}, + {title: '#AAAA00', value: '#AAAA00', expectedValue: '#aaaa00', + expectedText: '#aa0'}, + {title: '#aaaA00', value: '#aaaA00', expectedValue: '#aaaa00', + expectedText: '#aa0'}, + {title: '#BCBCBC', value: '#BCBCBC', expectedValue: '#bcbcbc', + expectedText: '#bcbcbc'}, + {title: '#bcbcbc', value: '#bcbcbc', expectedValue: '#bcbcbc', + expectedText: '#bcbcbc'}, + {title: '#AA0', value: '#AA0', expectedValue: '#aaaa00', + expectedText: '#aa0'}, + {title: '#aa0', value: '#aa0', expectedValue: '#aaaa00', + expectedText: '#aa0'}, + {title: 'rgb(170, 170, 0)', value: 'rgb(170, 170, 0)', + expectedValue: '#aaaa00', expectedText: '#aa0'}, + {title: 'red', value: 'red', expectedValue: '#ff0000', + expectedText: '#f00'}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = [testCase.value]; + testCase.json = {'colour': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = Blockly.FieldColour.COLOURS[0]; + /** + * The expected default text for the field being tested. + * @type {*} + */ + var defaultTextValue = ( + function() { + var expectedText = defaultFieldValue; + var m = defaultFieldValue.match(/^#(.)\1(.)\2(.)\3$/); + if (m) { + expectedText = '#' + m[1] + m[2] + m[3]; + } + return expectedText; + })(); + /** + * Asserts that the field property values are set to default. + * @param {FieldTemplate} field The field to check. + */ + var assertFieldDefault = function(field) { + testHelpers.assertFieldValue(field, defaultFieldValue, defaultTextValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldAngle} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + testHelpers.assertFieldValue( + field, testCase.expectedValue, testCase.expectedText); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldColour, validValueTestCases, invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldColour, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { suite('Empty -> New Value', function() { setup(function() { @@ -138,35 +103,40 @@ suite('Colour Fields', function() { }); test('Null', function() { this.colourField.setValue(null); - assertValueDefault(this.colourField); + assertFieldDefault(this.colourField); }); test('Undefined', function() { this.colourField.setValue(undefined); - assertValueDefault(this.colourField); + assertFieldDefault(this.colourField); }); test('Non-Parsable String', function() { this.colourField.setValue('not_a_colour'); - assertValueDefault(this.colourField); + assertFieldDefault(this.colourField); }); test('#000000', function() { this.colourField.setValue('#000000'); - assertValue(this.colourField, '#000000', '#000'); + testHelpers.assertFieldValue(this.colourField, '#000000', '#000'); }); test('#bcbcbc', function() { this.colourField.setValue('#bcbcbc'); - assertValue(this.colourField, '#bcbcbc', '#bcbcbc'); + testHelpers.assertFieldValue(this.colourField, '#bcbcbc', '#bcbcbc'); }); test('#aa0', function() { this.colourField.setValue('#aa0'); - assertValue(this.colourField, '#aaaa00', '#aa0'); + testHelpers.assertFieldValue(this.colourField, '#aaaa00', '#aa0'); }); test('rgb(170, 170, 0)', function() { this.colourField.setValue('rgb(170, 170, 0)'); - assertValue(this.colourField, '#aaaa00', '#aa0'); + testHelpers.assertFieldValue(this.colourField, '#aaaa00', '#aa0'); }); test('red', function() { this.colourField.setValue('red'); - assertValue(this.colourField, '#ff0000', '#f00'); + testHelpers.assertFieldValue(this.colourField, '#ff0000', '#f00'); + }); + test('With source block', function() { + this.colourField.setSourceBlock(createTestBlock()); + this.colourField.setValue('#bcbcbc'); + testHelpers.assertFieldValue(this.colourField, '#bcbcbc', '#bcbcbc'); }); }); suite('Value -> New Value', function() { @@ -175,75 +145,69 @@ suite('Colour Fields', function() { }); test('Null', function() { this.colourField.setValue(null); - assertValue(this.colourField, '#aaaaaa', '#aaa'); + testHelpers.assertFieldValue(this.colourField, '#aaaaaa', '#aaa'); }); test('Undefined', function() { this.colourField.setValue(undefined); - assertValue(this.colourField, '#aaaaaa', '#aaa'); + testHelpers.assertFieldValue(this.colourField, '#aaaaaa', '#aaa'); }); test('Non-Parsable String', function() { this.colourField.setValue('not_a_colour'); - assertValue(this.colourField, '#aaaaaa', '#aaa'); + testHelpers.assertFieldValue(this.colourField, '#aaaaaa', '#aaa'); }); test('#000000', function() { this.colourField.setValue('#000000'); - assertValue(this.colourField, '#000000', '#000'); + testHelpers.assertFieldValue(this.colourField, '#000000', '#000'); }); test('#bcbcbc', function() { this.colourField.setValue('#bcbcbc'); - assertValue(this.colourField, '#bcbcbc', '#bcbcbc'); + testHelpers.assertFieldValue(this.colourField, '#bcbcbc', '#bcbcbc'); }); test('#aa0', function() { this.colourField.setValue('#aa0'); - assertValue(this.colourField, '#aaaa00', '#aa0'); + testHelpers.assertFieldValue(this.colourField, '#aaaa00', '#aa0'); }); test('rgb(170, 170, 0)', function() { this.colourField.setValue('rgb(170, 170, 0)'); - assertValue(this.colourField, '#aaaa00', '#aa0'); + testHelpers.assertFieldValue(this.colourField, '#aaaa00', '#aa0'); }); test('red', function() { this.colourField.setValue('red'); - assertValue(this.colourField, '#ff0000', '#f00'); + testHelpers.assertFieldValue(this.colourField, '#ff0000', '#f00'); }); }); }); suite('Validators', function() { setup(function() { - this.colourField = new Blockly.FieldColour('#aaaaaa'); + this.field = new Blockly.FieldColour('#aaaaaa'); }); - teardown(function() { - this.colourField.setValidator(null); - }); - suite('Null Validator', function() { - setup(function() { - this.colourField.setValidator(function() { - return null; + var testSuites = [ + {title: 'Null Validator', + validator: + function() { + return null; + }, + value: '#000000', expectedValue: '#aaaaaa', expectedText: '#aaa'}, + {title: 'Force Full Red Validator', + validator: + function(newValue) { + return '#ff' + newValue.substr(3, 4); + }, + value: '#000000', expectedValue: '#ff0000', expectedText: '#f00'}, + {title: 'Returns Undefined Validator', validator: function() {}, + value: '#000000', expectedValue: '#000000', expectedText: '#000'}, + ]; + testSuites.forEach(function(suiteInfo) { + suite(suiteInfo.title, function() { + setup(function() { + this.field.setValidator(suiteInfo.validator); }); - }); - test('New Value', function() { - this.colourField.setValue('#000000'); - assertValue(this.colourField, '#aaaaaa', '#aaa'); - }); - }); - suite('Force Full Red Validator', function() { - setup(function() { - this.colourField.setValidator(function(newValue) { - return '#ff' + newValue.substr(3, 4); + test('New Value', function() { + this.field.setValue(suiteInfo.value); + testHelpers.assertFieldValue( + this.field, suiteInfo.expectedValue, suiteInfo.expectedText); }); }); - test('New Value', function() { - this.colourField.setValue('#000000'); - assertValue(this.colourField, '#ff0000', '#f00'); - }); - }); - suite('Returns Undefined Validator', function() { - setup(function() { - this.colourField.setValidator(function() {}); - }); - test('New Value', function() { - this.colourField.setValue('#000000'); - assertValue(this.colourField, '#000000', '#000'); - }); }); }); suite('Customizations', function() { diff --git a/tests/mocha/field_date_test.js b/tests/mocha/field_date_test.js deleted file mode 100644 index 50fd781f5..000000000 --- a/tests/mocha/field_date_test.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/* If you want to run date tests add the date picker here: - * https://github.com/google/blockly/blob/master/core/blockly.js#L41 - * before unskipping. - */ -suite.skip('Date Fields', function() { - function assertValue(dateField, expectedValue) { - var actualValue = dateField.getValue(); - var actualText = dateField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedValue); - } - function assertValueDefault(dateField) { - var today = new goog.date.Date().toIsoString(true); - assertValue(dateField, today); - } - suite('Constructor', function() { - test('Empty', function() { - var dateField = new Blockly.FieldDate(); - assertValueDefault(dateField); - }); - test('Undefined', function() { - var dateField = new Blockly.FieldDate(undefined); - assertValueDefault(dateField); - }); - test('Non-Parsable String', function() { - var dateField = new Blockly.FieldDate('bad'); - assertValueDefault(dateField); - }); - test('2020-02-20', function() { - var dateField = new Blockly.FieldDate('2020-02-20'); - assertValue(dateField, '2020-02-20'); - }); - test('Invalid Date - Month(2020-13-20)', function() { - var dateField = new Blockly.FieldDate('2020-13-20'); - assertValueDefault(dateField); - }); - test('Invalid Date - Day(2020-02-32)', function() { - var dateField = new Blockly.FieldDate('2020-02-32'); - assertValueDefault(dateField); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var dateField = Blockly.FieldDate.fromJson({}); - assertValueDefault(dateField); - }); - test('Undefined', function() { - var dateField = Blockly.FieldDate.fromJson({ date: undefined }); - assertValueDefault(dateField); - }); - test('Non-Parsable String', function() { - var dateField = Blockly.FieldDate.fromJson({ date: 'bad' }); - assertValueDefault(dateField); - }); - test('2020-02-20', function() { - var dateField = Blockly.FieldDate.fromJson({ date: '2020-02-20' }); - assertValue(dateField, '2020-02-20'); - }); - test('Invalid Date - Month(2020-13-20)', function() { - var dateField = Blockly.FieldDate.fromJson({ date: '2020-13-20' }); - assertValueDefault(dateField); - }); - test('Invalid Date - Day(2020-02-32)', function() { - var dateField = Blockly.FieldDate.fromJson({ date: '2020-02-32' }); - assertValueDefault(dateField); - }); - }); - suite('setValue', function() { - suite('Empty -> New Value', function() { - setup(function() { - this.dateField = new Blockly.FieldDate(); - }); - test('Null', function() { - this.dateField.setValue(null); - assertValueDefault(this.dateField); - }); - test('Undefined', function() { - this.dateField.setValue(undefined); - assertValueDefault(this.dateField); - }); - test('Non-Parsable String', function() { - this.dateField.setValue('bad'); - assertValueDefault(this.dateField); - }); - test('Invalid Date - Month(2020-13-20)', function() { - this.dateField.setValue('2020-13-20'); - assertValueDefault(this.dateField); - }); - test('Invalid Date - Day(2020-02-32)', function() { - this.dateField.setValue('2020-02-32'); - assertValueDefault(this.dateField); - }); - test('3030-03-30', function() { - this.dateField.setValue('3030-03-30'); - assertValue(this.dateField, '3030-03-30'); - }); - }); - suite('Value -> New Value', function() { - setup(function() { - this.dateField = new Blockly.FieldDate('2020-02-20'); - }); - test('Null', function() { - this.dateField.setValue(null); - assertValue(this.dateField, '2020-02-20'); - }); - test('Undefined', function() { - this.dateField.setValue(undefined); - assertValue(this.dateField, '2020-02-20'); - }); - test('Non-Parsable String', function() { - this.dateField.setValue('bad'); - assertValue(this.dateField, '2020-02-20'); - }); - test('Invalid Date - Month(2020-13-20)', function() { - this.dateField.setValue('2020-13-20'); - assertValue(this.dateField, '2020-02-20'); - }); - test('Invalid Date - Day(2020-02-32)', function() { - this.dateField.setValue('2020-02-32'); - assertValue(this.dateField, '2020-02-20'); - }); - test('3030-03-30', function() { - this.dateField.setValue('3030-03-30'); - assertValue(this.dateField, '3030-03-30'); - }); - }); - }); - suite('Validators', function() { - setup(function() { - this.dateField = new Blockly.FieldDate('2020-02-20'); - }); - teardown(function() { - this.dateField.setValidator(null); - }); - suite('Null Validator', function() { - setup(function() { - this.dateField.setValidator(function() { - return null; - }); - }); - test('New Value', function() { - this.dateField.setValue('3030-03-30'); - assertValue(this.dateField, '2020-02-20'); - }); - }); - suite('Force Day 20s Validator', function() { - setup(function() { - this.dateField.setValidator(function(newValue) { - return newValue.substr(0, 8) + '2' + newValue.substr(9, 1); - }); - }); - test('New Value', function() { - this.dateField.setValue('3030-03-30'); - assertValue(this.dateField, '3030-03-20'); - }); - }); - suite('Returns Undefined Validator', function() { - setup(function() { - this.dateField.setValidator(function() {}); - }); - test('New Value', function() { - this.dateField.setValue('3030-03-30'); - assertValue(this.dateField, '3030-03-30'); - }); - }); - }); -}); diff --git a/tests/mocha/field_dropdown_test.js b/tests/mocha/field_dropdown_test.js index 0bbc36724..9e126adc1 100644 --- a/tests/mocha/field_dropdown_test.js +++ b/tests/mocha/field_dropdown_test.js @@ -8,8 +8,8 @@ suite('Dropdown Fields', function() { function assertValue(dropdownField, expectedValue, expectedText) { var actualValue = dropdownField.getValue(); var actualText = dropdownField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedText); + chai.assert.equal(actualValue, expectedValue); + chai.assert.equal(actualText, expectedText); } suite('Constructor', function() { test('Empty', function() { @@ -148,6 +148,11 @@ suite('Dropdown Fields', function() { this.dropdownField.setValue('B'); assertValue(this.dropdownField, 'B', 'b'); }); + test('With source block', function() { + this.dropdownField.setSourceBlock(createTestBlock()); + this.dropdownField.setValue('B'); + assertValue(this.dropdownField, 'B', 'b'); + }); }); suite('Validators', function() { setup(function() { diff --git a/tests/mocha/field_image_test.js b/tests/mocha/field_image_test.js index e5113de1a..f2c415a84 100644 --- a/tests/mocha/field_image_test.js +++ b/tests/mocha/field_image_test.js @@ -8,8 +8,8 @@ suite('Image Fields', function() { function assertValue(imageField, expectedValue, expectedText) { var actualValue = imageField.getValue(); var actualText = imageField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedText); + chai.assert.equal(actualValue, expectedValue); + chai.assert.equal(actualText, expectedText); } suite('Constructor', function() { test('Empty', function() { @@ -136,7 +136,7 @@ suite('Image Fields', function() { test('Remove Click Handler', function() { var field = new Blockly.FieldImage('src', 10, 10, null, this.onClick); field.setOnClickHandler(null); - chai.assert.equal(field.clickHandler_, null); + chai.assert.isNull(field.clickHandler_); }); }); suite('Alt', function() { diff --git a/tests/mocha/field_label_serializable_test.js b/tests/mocha/field_label_serializable_test.js index fb490a6ee..8b7954a68 100644 --- a/tests/mocha/field_label_serializable_test.js +++ b/tests/mocha/field_label_serializable_test.js @@ -5,173 +5,109 @@ */ suite('Label Serializable Fields', function() { - function assertValue(labelField, expectedValue) { - var actualValue = labelField.getValue(); - var actualText = labelField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedValue); - } - function assertValueDefault(labelField) { - assertValue(labelField, ''); - } - function assertHasClass(labelField, cssClass) { - labelField.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); - labelField.constants_ = { - FIELD_TEXT_BASELINE_Y: 13 - }; - labelField.initView(); - chai.assert.isTrue(Blockly.utils.dom.hasClass( - labelField.textElement_, cssClass)); - } - function assertDoesNotHaveClass(labelField, cssClass) { - labelField.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); - labelField.constants_ = { - FIELD_TEXT_BASELINE_Y: 13 - }; - labelField.initView(); - chai.assert.isFalse(Blockly.utils.dom.hasClass( - labelField.textElement_, cssClass)); - } - suite('Constructor', function() { - test('Empty', function() { - var labelField = new Blockly.FieldLabelSerializable(); - assertValueDefault(labelField); - }); - test('Null', function() { - var labelField = new Blockly.FieldLabelSerializable(null); - assertValueDefault(labelField); - }); - test('Undefined', function() { - var labelField = new Blockly.FieldLabelSerializable(undefined); - assertValueDefault(labelField); - }); - test('String', function() { - var labelField = new Blockly.FieldLabelSerializable('value'); - assertValue(labelField, 'value'); - }); - test('Number (Truthy)', function() { - var labelField = new Blockly.FieldLabelSerializable(1); - assertValue(labelField, '1'); - }); - test('Number (Falsy)', function() { - var labelField = new Blockly.FieldLabelSerializable(0); - assertValue(labelField, '0'); - }); - test('Boolean True', function() { - var labelField = new Blockly.FieldLabelSerializable(true); - assertValue(labelField, 'true'); - }); - test('Boolean False', function() { - var labelField = new Blockly.FieldLabelSerializable(false); - assertValue(labelField, 'false'); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var labelField = new Blockly.FieldLabelSerializable.fromJson({}); - assertValueDefault(labelField); - }); - test('Null', function() { - var labelField = new Blockly.FieldLabelSerializable - .fromJson({ text:null }); - assertValueDefault(labelField); - }); - test('Undefined', function() { - var labelField = new Blockly.FieldLabelSerializable - .fromJson({ text:undefined }); - assertValueDefault(labelField); - }); - test('String', function() { - var labelField = Blockly.FieldLabelSerializable - .fromJson({ text:'value' }); - assertValue(labelField, 'value'); - }); - test('Number (Truthy)', function() { - var labelField = Blockly.FieldLabelSerializable.fromJson({ text:1 }); - assertValue(labelField, '1'); - }); - test('Number (Falsy)', function() { - var labelField = Blockly.FieldLabelSerializable.fromJson({ text:0 }); - assertValue(labelField, '0'); - }); - test('Boolean True', function() { - var labelField = Blockly.FieldLabelSerializable.fromJson({ text:true }); - assertValue(labelField, 'true'); - }); - test('Boolean False', function() { - var labelField = Blockly.FieldLabelSerializable.fromJson({ text:false }); - assertValue(labelField, 'false'); - }); - }); + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + var validValueTestCases = [ + {title: 'String', value: 'value', expectedValue: 'value'}, + {title: 'Boolean true', value: true, expectedValue: 'true'}, + {title: 'Boolean false', value: false, expectedValue: 'false'}, + {title: 'Number (Truthy)', value: 1, expectedValue: '1'}, + {title: 'Number (Falsy)', value: 0, expectedValue: '0'}, + {title: 'NaN', value: NaN, expectedValue: 'NaN'}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = [testCase.value]; + testCase.json = {'text': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = ''; + /** + * Asserts that the field property values are set to default. + * @param {!Blockly.FieldNumber} field The field to check. + */ + var assertFieldDefault = function(field) { + testHelpers.assertFieldValue(field, defaultFieldValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldNumber} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + testHelpers.assertFieldValue(field, testCase.expectedValue); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldLabelSerializable, validValueTestCases, + invalidValueTestCases, validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldLabelSerializable, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { suite('Empty -> New Value', function() { setup(function() { - this.labelField = new Blockly.FieldLabelSerializable(); + this.field = new Blockly.FieldLabelSerializable(); }); - test('Null', function() { - this.labelField.setValue(null); - assertValueDefault(this.labelField); - }); - test('Undefined', function() { - this.labelField.setValue(undefined); - assertValueDefault(this.labelField); - }); - test('New String', function() { - this.labelField.setValue('newValue'); - assertValue(this.labelField, 'newValue'); - }); - test('Number (Truthy)', function() { - this.labelField.setValue(1); - assertValue(this.labelField, '1'); - }); - test('Number (Falsy)', function() { - this.labelField.setValue(0); - assertValue(this.labelField, '0'); - }); - test('Boolean True', function() { - this.labelField.setValue(true); - assertValue(this.labelField, 'true'); - }); - test('Boolean False', function() { - this.labelField.setValue(false); - assertValue(this.labelField, 'false'); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, defaultFieldValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + testHelpers.assertFieldValue(this.field, 'value'); }); }); suite('Value -> New Value', function() { + var initialValue = 'oldValue'; setup(function() { - this.labelField = new Blockly.FieldLabelSerializable('value'); + this.field = new Blockly.FieldLabelSerializable(initialValue); }); - test('Null', function() { - this.labelField.setValue(null); - assertValue(this.labelField, 'value'); - }); - test('Undefined', function() { - this.labelField.setValue(undefined); - assertValue(this.labelField, 'value'); - }); - test('New String', function() { - this.labelField.setValue('newValue'); - assertValue(this.labelField, 'newValue'); - }); - test('Number (Truthy)', function() { - this.labelField.setValue(1); - assertValue(this.labelField, '1'); - }); - test('Number (Falsy)', function() { - this.labelField.setValue(0); - assertValue(this.labelField, '0'); - }); - test('Boolean True', function() { - this.labelField.setValue(true); - assertValue(this.labelField, 'true'); - }); - test('Boolean False', function() { - this.labelField.setValue(false); - assertValue(this.labelField, 'false'); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, initialValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + testHelpers.assertFieldValue(this.field, 'value'); }); }); }); + suite('Customizations', function() { + function assertHasClass(labelField, cssClass) { + labelField.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); + labelField.constants_ = { + FIELD_TEXT_BASELINE_Y: 13 + }; + labelField.initView(); + chai.assert.isTrue(Blockly.utils.dom.hasClass( + labelField.textElement_, cssClass)); + } + function assertDoesNotHaveClass(labelField, cssClass) { + labelField.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); + labelField.constants_ = { + FIELD_TEXT_BASELINE_Y: 13 + }; + labelField.initView(); + chai.assert.isFalse(Blockly.utils.dom.hasClass( + labelField.textElement_, cssClass)); + } test('JS Constructor', function() { var field = new Blockly.FieldLabelSerializable('text', 'testClass'); assertHasClass(field, 'testClass'); diff --git a/tests/mocha/field_label_test.js b/tests/mocha/field_label_test.js index b2cf8be95..62a888aae 100644 --- a/tests/mocha/field_label_test.js +++ b/tests/mocha/field_label_test.js @@ -5,162 +5,112 @@ */ suite('Label Fields', function() { - function assertValue(labelField, expectedValue) { - var actualValue = labelField.getValue(); - var actualText = labelField.getText(); - assertEquals(actualValue, expectedValue); - assertEquals(actualText, expectedValue); - } - function assertValueDefault(labelField) { - assertValue(labelField, ''); - } - function assertHasClass(labelField, cssClass) { - labelField.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); - labelField.constants_ = { - FIELD_TEXT_BASELINE_Y: 13 - }; - labelField.initView(); - chai.assert.isTrue(Blockly.utils.dom.hasClass( - labelField.textElement_, cssClass)); - } - function assertDoesNotHaveClass(labelField, cssClass) { - labelField.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); - labelField.constants_ = { - FIELD_TEXT_BASELINE_Y: 13 - }; - labelField.initView(); - chai.assert.isFalse(Blockly.utils.dom.hasClass( - labelField.textElement_, cssClass)); - } - suite('Constructor', function() { - test('Empty', function() { - var labelField = new Blockly.FieldLabel(); - assertValueDefault(labelField); - }); - test('Undefined', function() { - var labelField = new Blockly.FieldLabel(undefined); - assertValueDefault(labelField); - }); - test('String', function() { - var labelField = new Blockly.FieldLabel('value'); - assertValue(labelField, 'value'); - }); - test('Number (Truthy)', function() { - var labelField = new Blockly.FieldLabel(1); - assertValue(labelField, '1'); - }); - test('Number (Falsy)', function() { - var labelField = new Blockly.FieldLabel(0); - assertValue(labelField, '0'); - }); - test('Boolean True', function() { - var labelField = new Blockly.FieldLabel(true); - assertValue(labelField, 'true'); - }); - test('Boolean False', function() { - var labelField = new Blockly.FieldLabel(false); - assertValue(labelField, 'false'); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var labelField = new Blockly.FieldLabel.fromJson({}); - assertValueDefault(labelField); - }); - test('Undefined', function() { - var labelField = new Blockly.FieldLabel.fromJson({ text:undefined }); - assertValueDefault(labelField); - }); - test('String', function() { - var labelField = Blockly.FieldLabel.fromJson({ text:'value' }); - assertValue(labelField, 'value'); - }); - test('Number (Truthy)', function() { - var labelField = Blockly.FieldLabel.fromJson({ text:1 }); - assertValue(labelField, '1'); - }); - test('Number (Falsy)', function() { - var labelField = Blockly.FieldLabel.fromJson({ text:0 }); - assertValue(labelField, '0'); - }); - test('Boolean True', function() { - var labelField = Blockly.FieldLabel.fromJson({ text:true }); - assertValue(labelField, 'true'); - }); - test('Boolean False', function() { - var labelField = Blockly.FieldLabel.fromJson({ text:false }); - assertValue(labelField, 'false'); - }); - }); + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + var validValueTestCases = [ + {title: 'String', value: 'value', expectedValue: 'value'}, + {title: 'Boolean true', value: true, expectedValue: 'true'}, + {title: 'Boolean false', value: false, expectedValue: 'false'}, + {title: 'Number (Truthy)', value: 1, expectedValue: '1'}, + {title: 'Number (Falsy)', value: 0, expectedValue: '0'}, + {title: 'NaN', value: NaN, expectedValue: 'NaN'}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = [testCase.value]; + testCase.json = {'text': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = ''; + /** + * Asserts that the field property values are set to default. + * @param {!Blockly.FieldNumber} field The field to check. + */ + var assertFieldDefault = function(field) { + testHelpers.assertFieldValue(field, defaultFieldValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldNumber} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + testHelpers.assertFieldValue(field, testCase.expectedValue); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldLabel, validValueTestCases, invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldLabel, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { suite('Empty -> New Value', function() { setup(function() { - this.labelField = new Blockly.FieldLabel(); + this.field = new Blockly.FieldLabel(); }); - test('Null', function() { - this.labelField.setValue(null); - assertValueDefault(this.labelField); - }); - test('Undefined', function() { - this.labelField.setValue(undefined); - assertValueDefault(this.labelField); - }); - test('New String', function() { - this.labelField.setValue('newValue'); - assertValue(this.labelField, 'newValue'); - }); - test('Number (Truthy)', function() { - this.labelField.setValue(1); - assertValue(this.labelField, '1'); - }); - test('Number (Falsy)', function() { - this.labelField.setValue(0); - assertValue(this.labelField, '0'); - }); - test('Boolean True', function() { - this.labelField.setValue(true); - assertValue(this.labelField, 'true'); - }); - test('Boolean False', function() { - this.labelField.setValue(false); - assertValue(this.labelField, 'false'); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, defaultFieldValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + testHelpers.assertFieldValue(this.field, 'value'); }); }); suite('Value -> New Value', function() { + var initialValue = 'oldValue'; setup(function() { - this.labelField = new Blockly.FieldLabel('value'); + this.field = new Blockly.FieldLabel(initialValue); }); - test('Null', function() { - this.labelField.setValue(null); - assertValue(this.labelField, 'value'); - }); - test('Undefined', function() { - this.labelField.setValue(undefined); - assertValue(this.labelField, 'value'); - }); - test('New String', function() { - this.labelField.setValue('newValue'); - assertValue(this.labelField, 'newValue'); - }); - test('Number (Truthy)', function() { - this.labelField.setValue(1); - assertValue(this.labelField, '1'); - }); - test('Number (Falsy)', function() { - this.labelField.setValue(0); - assertValue(this.labelField, '0'); - }); - test('Boolean True', function() { - this.labelField.setValue(true); - assertValue(this.labelField, 'true'); - }); - test('Boolean False', function() { - this.labelField.setValue(false); - assertValue(this.labelField, 'false'); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, initialValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + testHelpers.assertFieldValue(this.field, 'value'); }); }); }); + suite('Customizations', function() { + function assertHasClass(labelField, cssClass) { + labelField.fieldGroup_ = + Blockly.utils.dom.createSvgElement('g', {}, null); + labelField.constants_ = { + FIELD_TEXT_BASELINE_Y: 13 + }; + labelField.initView(); + chai.assert.isTrue(Blockly.utils.dom.hasClass( + labelField.textElement_, cssClass)); + } + function assertDoesNotHaveClass(labelField, cssClass) { + labelField.fieldGroup_ = + Blockly.utils.dom.createSvgElement('g', {}, null); + labelField.constants_ = { + FIELD_TEXT_BASELINE_Y: 13 + }; + labelField.initView(); + chai.assert.isFalse(Blockly.utils.dom.hasClass( + labelField.textElement_, cssClass)); + } + test('JS Constructor', function() { var field = new Blockly.FieldLabel('text', 'testClass'); assertHasClass(field, 'testClass'); diff --git a/tests/mocha/field_number_test.js b/tests/mocha/field_number_test.js index 91edca652..b18bb1c0e 100644 --- a/tests/mocha/field_number_test.js +++ b/tests/mocha/field_number_test.js @@ -5,378 +5,212 @@ */ suite('Number Fields', function() { - function assertValue(numberField, expectedValue, opt_expectedText) { - var actualValue = numberField.getValue(); - var actualText = numberField.getText(); - opt_expectedText = opt_expectedText || String(expectedValue); - assertEquals(String(actualValue), String(expectedValue)); - assertEquals(Number(actualValue), expectedValue); - assertEquals(actualText, opt_expectedText); - } - function assertValueDefault(numberField) { - assertValue(numberField, 0); - } - function assertNumberField(numberField, expectedMin, expectedMax, + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + {title: 'NaN', value: NaN}, + {title: 'Non-Parsable String', value: 'bad'}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + var validValueTestCases = [ + {title: 'Integer', value: 1, expectedValue: 1}, + {title: 'Float', value: 1.5, expectedValue: 1.5}, + {title: 'Integer String', value: '1', expectedValue: 1}, + {title: 'Float String', value: '1.5', expectedValue: 1.5}, + {title: 'Infinity', value: Infinity, expectedValue: Infinity}, + {title: 'Negative Infinity', value: -Infinity, expectedValue: -Infinity}, + {title: 'Infinity String', value: 'Infinity', expectedValue: Infinity}, + {title: 'Negative Infinity String', value: '-Infinity', + expectedValue: -Infinity}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = Array(4).fill(testCase.value); + testCase.json = {'value': testCase.value, 'min': testCase.value, + 'max': testCase.value, 'precision': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = 0; + /** + * Asserts that the field property values are as expected. + * @param {!Blockly.FieldNumber} field The field to check. + * @param {!number} expectedMin The expected min. + * @param {!number} expectedMax The expected max. + * @param {!number} expectedPrecision The expected precision. + * @param {!number} expectedValue The expected value. + */ + function assertNumberField(field, expectedMin, expectedMax, expectedPrecision, expectedValue) { - assertValue(numberField, expectedValue); - chai.assert.equal(numberField.getMin(), expectedMin); - chai.assert.equal(numberField.getMax(), expectedMax); - chai.assert.equal(numberField.getPrecision(), expectedPrecision); + testHelpers.assertFieldValue(field, expectedValue); + chai.assert.equal(field.getMin(), expectedMin, 'Min'); + chai.assert.equal(field.getMax(), expectedMax, 'Max'); + chai.assert.equal( + field.getPrecision(), expectedPrecision, 'Precision'); } - function assertNumberFieldDefault(numberField) { - assertNumberField(numberField, -Infinity, Infinity, 0, 0); - } - function createNumberFieldSameValuesConstructor(value) { - return new Blockly.FieldNumber(value, value, value, value); - } - function createNumberFieldSameValuesJson(value) { - return Blockly.FieldNumber.fromJson( - { 'value': value, min: value, max: value, precision: value }); - } - function assertNumberFieldSameValues(numberField, value) { - assertNumberField(numberField, value, value, value, value); - } - suite('Constructor', function() { - test('Empty', function() { - var numberField = new Blockly.FieldNumber(); - assertNumberFieldDefault(numberField); - }); - test('Undefined', function() { - var numberField = createNumberFieldSameValuesConstructor(undefined); - assertNumberFieldDefault(numberField); - }); - test('NaN', function() { - var numberField = createNumberFieldSameValuesConstructor(NaN); - assertNumberFieldDefault(numberField); - }); - test('Integer', function() { - var numberField = createNumberFieldSameValuesConstructor(1); - assertNumberFieldSameValues(numberField, 1); - }); - test('Float', function() { - var numberField = createNumberFieldSameValuesConstructor(1.5); - assertNumberFieldSameValues(numberField, 1.5); - }); - test('Integer String', function() { - var numberField = createNumberFieldSameValuesConstructor('1'); - assertNumberFieldSameValues(numberField, 1); - }); - test('Float String', function() { - var numberField = createNumberFieldSameValuesConstructor('1.5'); - assertNumberFieldSameValues(numberField, 1.5); - }); - test('Infinity', function() { - var numberField = createNumberFieldSameValuesConstructor('Infinity'); - assertNumberFieldSameValues(numberField, Infinity); - }); - test('Negative Infinity String', function() { - var numberField = createNumberFieldSameValuesConstructor('-Infinity'); - assertNumberFieldSameValues(numberField, -Infinity); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var numberField = Blockly.FieldNumber.fromJson({}); - assertNumberFieldDefault(numberField); - }); - test('Undefined', function() { - var numberField = createNumberFieldSameValuesJson(undefined); - assertNumberFieldDefault(numberField); - }); - test('NaN', function() { - var numberField = createNumberFieldSameValuesJson(NaN); - assertNumberFieldDefault(numberField); - }); - test('Integer', function() { - var numberField = createNumberFieldSameValuesJson(1); - assertNumberFieldSameValues(numberField, 1); - }); - test('Float', function() { - var numberField = createNumberFieldSameValuesJson(1.5); - assertNumberFieldSameValues(numberField, 1.5); - }); - test('Integer String', function() { - var numberField = createNumberFieldSameValuesJson('1'); - assertNumberFieldSameValues(numberField, 1); - }); - test('Float String', function() { - var numberField = createNumberFieldSameValuesJson('1.5'); - assertNumberFieldSameValues(numberField, 1.5); - }); - test('Infinity', function() { - var numberField = createNumberFieldSameValuesJson('Infinity'); - assertNumberFieldSameValues(numberField, Infinity); - }); - test('Negative Infinity String', function() { - var numberField = createNumberFieldSameValuesJson('-Infinity'); - assertNumberFieldSameValues(numberField, -Infinity); - }); - }); + /** + * Asserts that the field property values are set to default. + * @param {!Blockly.FieldNumber} field The field to check. + */ + var assertFieldDefault = function(field) { + assertNumberField(field, -Infinity, Infinity, 0, defaultFieldValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldNumber} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + assertNumberField( + field, testCase.expectedValue, testCase.expectedValue, + testCase.expectedValue, testCase.expectedValue); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldNumber, validValueTestCases, invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldNumber, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { - suite('Value Types', function() { - suite('Empty -> New Value', function() { - setup(function() { - this.numberField = new Blockly.FieldNumber(); - }); - test('Null', function() { - this.numberField.setValue(null); - assertValueDefault(this.numberField); - }); - test('Undefined', function() { - this.numberField.setValue(undefined); - assertValueDefault(this.numberField); - }); - test('Non-Parsable String', function() { - this.numberField.setValue('bad'); - assertValueDefault(this.numberField); - }); - test('NaN', function() { - this.numberField.setValue(NaN); - assertValueDefault(this.numberField); - }); - test('Integer', function() { - this.numberField.setValue(2); - assertValue(this.numberField, 2); - }); - test('Float', function() { - this.numberField.setValue(2.5); - assertValue(this.numberField, 2.5); - }); - test('Integer String', function() { - this.numberField.setValue('2'); - assertValue(this.numberField, 2); - }); - test('Float String', function() { - this.numberField.setValue('2.5'); - assertValue(this.numberField, 2.5); - }); - test('Infinity', function() { - this.numberField.setValue(Infinity); - assertValue(this.numberField, Infinity); - }); - test('Negative Infinity String', function() { - this.numberField.setValue('-Infinity'); - assertValue(this.numberField, -Infinity); - }); + suite('Empty -> New Value', function() { + setup(function() { + this.field = new Blockly.FieldNumber(); }); - suite('Value -> New Value', function() { - setup(function() { - this.numberField = new Blockly.FieldNumber(1); - }); - test('Null', function() { - this.numberField.setValue(null); - assertValue(this.numberField, 1); - }); - test('Undefined', function() { - this.numberField.setValue(undefined); - assertValue(this.numberField, 1); - }); - test('Non-Parsable String', function() { - this.numberField.setValue('bad'); - assertValue(this.numberField, 1); - }); - test('NaN', function() { - this.numberField.setValue(NaN); - assertValue(this.numberField, 1); - }); - test('Integer', function() { - this.numberField.setValue(2); - assertValue(this.numberField, 2); - }); - test('Float', function() { - this.numberField.setValue(2.5); - assertValue(this.numberField, 2.5); - }); - test('Integer String', function() { - this.numberField.setValue('2'); - assertValue(this.numberField, 2); - }); - test('Float String', function() { - this.numberField.setValue('2.5'); - assertValue(this.numberField, 2.5); - }); - test('Infinity', function() { - this.numberField.setValue(Infinity); - assertValue(this.numberField, Infinity); - }); - test('Negative Infinity String', function() { - this.numberField.setValue('-Infinity'); - assertValue(this.numberField, -Infinity); - }); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, defaultFieldValue); + }); + suite('Value -> New Value', function() { + var initialValue = 1; + setup(function() { + this.field = new Blockly.FieldNumber(initialValue); }); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, initialValue); }); suite('Constraints', function() { + var testCases = [ + {title: 'Float', json: {}, value: 123.456, expectedValue: 123.456}, + {title: '0.01', json: {precision: .01}, value: 123.456, + expectedValue: 123.46}, + {title: '0.5', json: {precision: .5}, value: 123.456, + expectedValue: 123.5}, + {title: '1', json: {precision: 1}, value: 123.456, + expectedValue: 123}, + {title: '1.5', json: {precision: 1.5}, value: 123.456, + expectedValue: 123}, + ]; suite('Precision', function() { - test('Float', function() { - var numberField = new Blockly.FieldNumber(); - numberField.setValue(123.456); - assertValue(numberField, 123.456); - }); - test('0.01', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ precision: .01 }); - numberField.setValue(123.456); - assertValue(numberField, 123.46); - }); - test('0.5', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ precision: .5 }); - numberField.setValue(123.456); - assertValue(numberField, 123.5); - }); - test('1', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ precision: 1 }); - numberField.setValue(123.456); - assertValue(numberField, 123); - }); - test('1.5', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ precision: 1.5 }); - numberField.setValue(123.456); - assertValue(numberField, 123); + testHelpers.runTestCases(testCases, function(testCase) { + return function() { + var field = Blockly.FieldNumber.fromJson(testCase.json); + field.setValue(testCase.value); + testHelpers.assertFieldValue(field, testCase.expectedValue); + }; }); test('Null', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ precision: null}); - assertEquals(numberField.getPrecision(), 0); + var field = Blockly.FieldNumber.fromJson({precision: null}); + chai.assert.equal(field.getPrecision(), 0); }); }); + var setValueBoundsTestFn = function(testCase) { + return function() { + var field = Blockly.FieldNumber.fromJson(testCase.json); + testCase.values.forEach(function(value, i) { + field.setValue(value); + testHelpers.assertFieldValue( + field, testCase.expectedValues[i]); + }); + }; + }; suite('Min', function() { - test('-10', function() { - var numberField = new Blockly.FieldNumber.fromJson({ min: -10 }); - numberField.setValue(-20); - assertValue(numberField, -10); - numberField.setValue(0); - assertValue(numberField, 0); - numberField.setValue(20); - assertValue(numberField, 20); - }); - test('0', function() { - var numberField = new Blockly.FieldNumber.fromJson({ min: 0 }); - numberField.setValue(-20); - assertValue(numberField, 0); - numberField.setValue(0); - assertValue(numberField, 0); - numberField.setValue(20); - assertValue(numberField, 20); - }); - test('+10', function() { - var numberField = new Blockly.FieldNumber.fromJson({ min: 10 }); - numberField.setValue(-20); - assertValue(numberField, 10); - numberField.setValue(0); - assertValue(numberField, 10); - numberField.setValue(20); - assertValue(numberField, 20); - }); + var testCases = [ + {title: '-10', json: {min: -10}, values: [-20, 0, 20], + expectedValues: [-10, 0, 20]}, + {title: '0', json: {min: 0}, values: [-20, 0, 20], + expectedValues: [0, 0, 20]}, + {title: '+10', json: {min: 10}, values: [-20, 0, 20], + expectedValues: [10, 10, 20]}, + ]; + testHelpers.runTestCases(testCases, setValueBoundsTestFn); test('Null', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ min: null}); - assertEquals(numberField.getMin(), -Infinity); + var field = Blockly.FieldNumber.fromJson({min: null}); + chai.assert.equal(field.getMin(), -Infinity); }); }); suite('Max', function() { - test('-10', function() { - var numberField = new Blockly.FieldNumber.fromJson({ max: -10 }); - numberField.setValue(-20); - assertValue(numberField, -20); - numberField.setValue(0); - assertValue(numberField, -10); - numberField.setValue(20); - assertValue(numberField, -10); - }); - test('0', function() { - var numberField = new Blockly.FieldNumber.fromJson({ max: 0 }); - numberField.setValue(-20); - assertValue(numberField, -20); - numberField.setValue(0); - assertValue(numberField, 0); - numberField.setValue(20); - assertValue(numberField, 0); - }); - test('+10', function() { - var numberField = new Blockly.FieldNumber.fromJson({ max: 10 }); - numberField.setValue(-20); - assertValue(numberField, -20); - numberField.setValue(0); - assertValue(numberField, 0); - numberField.setValue(20); - assertValue(numberField, 10); - }); - test('null', function() { - var numberField = new Blockly.FieldNumber - .fromJson({ max: null}); - assertEquals(numberField.getMax(), Infinity); + var testCases = [ + {title: '-10', json: {max: -10}, values: [-20, 0, 20], + expectedValues: [-20, -10, -10]}, + {title: '0', json: {max: 0}, values: [-20, 0, 20], + expectedValues: [-20, 0, 0]}, + {title: '+10', json: {max: 10}, values: [-20, 0, 20], + expectedValues: [-20, 0, 10]}, + ]; + testHelpers.runTestCases(testCases, setValueBoundsTestFn); + test('Null', function() { + var field = Blockly.FieldNumber.fromJson({max: null}); + chai.assert.equal(field.getMax(), Infinity); }); }); }); }); suite('Validators', function() { setup(function() { - this.numberField = new Blockly.FieldNumber(1); - this.numberField.htmlInput_ = Object.create(null); - this.numberField.htmlInput_.oldValue_ = '1'; - this.numberField.htmlInput_.untypedDefaultValue_ = 1; - this.stub = sinon.stub(this.numberField, 'resizeEditor_'); + this.field = new Blockly.FieldNumber(1); + this.field.htmlInput_ = Object.create(null); + this.field.htmlInput_.oldValue_ = '1'; + this.field.htmlInput_.untypedDefaultValue_ = 1; + this.stub = sinon.stub(this.field, 'resizeEditor_'); }); teardown(function() { - this.numberField.setValidator(null); - this.numberField.htmlInput_ = null; - if (this.stub) { - this.stub.restore(); - } + sinon.restore(); }); - suite('Null Validator', function() { - setup(function() { - this.numberField.setValidator(function() { - return null; + var testSuites = [ + {title: 'Null Validator', + validator: + function() { + return null; + }, + value: 2, expectedValue: 1}, + {title: 'Force End with 6 Validator', + validator: + function(newValue) { + return String(newValue).replace(/.$/, '6'); + }, + value: 25, expectedValue: 26}, + {title: 'Returns Undefined Validator', validator: function() {}, value: 2, + expectedValue: 2}, + ]; + testSuites.forEach(function(suiteInfo) { + suite(suiteInfo.title, function() { + setup(function() { + this.field.setValidator(suiteInfo.validator); }); - }); - test('When Editing', function() { - this.numberField.isBeingEdited_ = true; - this.numberField.htmlInput_.value = '2'; - this.numberField.onHtmlInputChange_(null); - assertValue(this.numberField, 1, '2'); - this.numberField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.numberField.setValue(2); - assertValue(this.numberField, 1); - }); - }); - suite('Force End with 6 Validator', function() { - setup(function() { - this.numberField.setValidator(function(newValue) { - return String(newValue).replace(/.$/, "6"); + test('When Editing', function() { + this.field.isBeingEdited_ = true; + this.field.htmlInput_.value = String(suiteInfo.value); + this.field.onHtmlInputChange_(null); + testHelpers.assertFieldValue( + this.field, suiteInfo.expectedValue, String(suiteInfo.value)); + }); + test('When Not Editing', function() { + this.field.setValue(suiteInfo.value); + testHelpers.assertFieldValue(this.field, suiteInfo.expectedValue); }); - }); - test('When Editing', function() { - this.numberField.isBeingEdited_ = true; - this.numberField.htmlInput_.value = '25'; - this.numberField.onHtmlInputChange_(null); - assertValue(this.numberField, 26, '25'); - this.numberField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.numberField.setValue(25); - assertValue(this.numberField, 26); - }); - }); - suite('Returns Undefined Validator', function() { - setup(function() { - this.numberField.setValidator(function() {}); - }); - test('When Editing', function() { - this.numberField.isBeingEdited_ = true; - this.numberField.htmlInput_.value = '2'; - this.numberField.onHtmlInputChange_(null); - assertValue(this.numberField, 2); - this.numberField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.numberField.setValue(2); - assertValue(this.numberField, 2); }); }); }); diff --git a/tests/mocha/field_registry_test.js b/tests/mocha/field_registry_test.js index c8ea3bb2f..eb0d3491e 100644 --- a/tests/mocha/field_registry_test.js +++ b/tests/mocha/field_registry_test.js @@ -20,46 +20,25 @@ suite('Field Registry', function() { }; teardown(function() { - if (Blockly.fieldRegistry.typeMap_['field_custom_test']) { - delete Blockly.fieldRegistry.typeMap_['field_custom_test']; + if (Blockly.registry.typeMap_['field']['field_custom_test']) { + delete Blockly.registry.typeMap_['field']['field_custom_test']; } }); suite('Registration', function() { test('Simple', function() { Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); }); - test('Empty String Key', function() { - chai.assert.throws(function() { - Blockly.fieldRegistry.register('', CustomFieldType); - }, 'Invalid field type'); - }); - test('Class as Key', function() { - chai.assert.throws(function() { - Blockly.fieldRegistry.register(CustomFieldType, ''); - }, 'Invalid field type'); - }); test('fromJson as Key', function() { chai.assert.throws(function() { Blockly.fieldRegistry.register(CustomFieldType.fromJson, ''); - }, 'Invalid field type'); - }); - test('Overwrite a Key', function() { - Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); - chai.assert.throws(function() { - Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); - }, 'already registered'); - }); - test('Null Value', function() { - chai.assert.throws(function() { - Blockly.fieldRegistry.register('field_custom_test', null); - }, 'fromJson function'); + }, 'Invalid name'); }); test('No fromJson', function() { var fromJson = CustomFieldType.fromJson; delete CustomFieldType.fromJson; chai.assert.throws(function() { Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); - }, 'fromJson function'); + }, 'must have a fromJson function'); CustomFieldType.fromJson = fromJson; }); test('fromJson not a function', function() { @@ -67,7 +46,7 @@ suite('Field Registry', function() { CustomFieldType.fromJson = true; chai.assert.throws(function() { Blockly.fieldRegistry.register('field_custom_test', CustomFieldType); - }, 'fromJson function'); + }, 'must have a fromJson function'); CustomFieldType.fromJson = fromJson; }); }); @@ -82,7 +61,7 @@ suite('Field Registry', function() { var field = Blockly.fieldRegistry.fromJson(json); chai.assert.isNotNull(field); - chai.assert.equal('ok', field.getValue()); + chai.assert.equal(field.getValue(), 'ok'); }); test('Not Registered', function() { var json = { @@ -106,7 +85,7 @@ suite('Field Registry', function() { var field = Blockly.fieldRegistry.fromJson(json); chai.assert.isNotNull(field); - chai.assert.equal('ok', field.getValue()); + chai.assert.equal(field.getValue(), 'ok'); }); }); }); diff --git a/tests/mocha/field_test.js b/tests/mocha/field_test.js index 553122fce..1381fb515 100644 --- a/tests/mocha/field_test.js +++ b/tests/mocha/field_test.js @@ -36,26 +36,26 @@ suite('Abstract Fields', function() { // An old default field should be serialized. var field = new FieldDefault(); var stub = sinon.stub(console, 'warn'); - assertEquals(true, field.isSerializable()); + chai.assert.isTrue(field.isSerializable()); chai.assert(stub.calledOnce); stub.restore(); }); test('Editable False, Serializable Default(false)', function() { // An old non-editable field should not be serialized. var field = new FieldFalseDefault(); - assertEquals(false, field.isSerializable()); + chai.assert.isFalse(field.isSerializable()); }); /* Test Other Cases */ test('Editable Default(true), Serializable True', function() { // A field that is both editable and serializable should be serialized. var field = new FieldDefaultTrue(); - assertEquals(true, field.isSerializable()); + chai.assert.isTrue(field.isSerializable()); }); test('Editable False, Serializable True', function() { // A field that is not editable, but overrides serializable to true // should be serialized (e.g. field_label_serializable) var field = new FieldFalseTrue(); - assertEquals(true, field.isSerializable()); + chai.assert.isTrue(field.isSerializable()); }); }); suite('setValue', function() { diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index a3acdec61..f51c1bf92 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -5,213 +5,137 @@ */ suite('Text Input Fields', function() { - function assertValue(textInputField, expectedValue, opt_expectedText) { - var actualValue = textInputField.getValue(); - var actualText = textInputField.getText(); - opt_expectedText = opt_expectedText || expectedValue; - assertEquals(actualValue, expectedValue); - assertEquals(actualText, opt_expectedText); - } - function assertValueDefault(textInputField) { - assertValue(textInputField, ''); - } - suite('Constructor', function() { - test('Empty', function() { - var textInputField = new Blockly.FieldTextInput(); - assertValueDefault(textInputField); - }); - test('Undefined', function() { - var textInputField = new Blockly.FieldTextInput(undefined); - assertValueDefault(textInputField); - }); - test('String', function() { - var textInputField = new Blockly.FieldTextInput('value'); - assertValue(textInputField, 'value'); - }); - test('Number (Truthy)', function() { - var textInputField = new Blockly.FieldTextInput(1); - assertValue(textInputField, '1'); - }); - test('Number (Falsy)', function() { - var textInputField = new Blockly.FieldTextInput(0); - assertValue(textInputField, '0'); - }); - test('Boolean True', function() { - var textInputField = new Blockly.FieldTextInput(true); - assertValue(textInputField, 'true'); - }); - test('Boolean False', function() { - var textInputField = new Blockly.FieldTextInput(false); - assertValue(textInputField, 'false'); - }); - }); - suite('fromJson', function() { - test('Empty', function() { - var textInputField = new Blockly.FieldTextInput.fromJson({}); - assertValueDefault(textInputField); - }); - test('Undefined', function() { - var textInputField = new Blockly.FieldTextInput - .fromJson({ text: undefined}); - assertValueDefault(textInputField); - }); - test('String', function() { - var textInputField = Blockly.FieldTextInput.fromJson({ text:'value' }); - assertValue(textInputField, 'value'); - }); - test('Number (Truthy)', function() { - var textInputField = Blockly.FieldTextInput.fromJson({ text:1 }); - assertValue(textInputField, '1'); - }); - test('Number (Falsy)', function() { - var textInputField = Blockly.FieldTextInput.fromJson({ text:0 }); - assertValue(textInputField, '0'); - }); - test('Boolean True', function() { - var textInputField = Blockly.FieldTextInput.fromJson({ text:true }); - assertValue(textInputField, 'true'); - }); - test('Boolean False', function() { - var textInputField = Blockly.FieldTextInput.fromJson({ text:false }); - assertValue(textInputField, 'false'); - }); - }); + /** + * Configuration for field tests with invalid values. + * @type {!Array} + */ + var invalidValueTestCases = [ + {title: 'Undefined', value: undefined}, + {title: 'Null', value: null}, + ]; + /** + * Configuration for field tests with valid values. + * @type {!Array} + */ + var validValueTestCases = [ + {title: 'String', value: 'value', expectedValue: 'value'}, + {title: 'Boolean true', value: true, expectedValue: 'true'}, + {title: 'Boolean false', value: false, expectedValue: 'false'}, + {title: 'Number (Truthy)', value: 1, expectedValue: '1'}, + {title: 'Number (Falsy)', value: 0, expectedValue: '0'}, + {title: 'NaN', value: NaN, expectedValue: 'NaN'}, + ]; + var addArgsAndJson = function(testCase) { + testCase.args = [testCase.value]; + testCase.json = {'text': testCase.value}; + }; + invalidValueTestCases.forEach(addArgsAndJson); + validValueTestCases.forEach(addArgsAndJson); + + /** + * The expected default value for the field being tested. + * @type {*} + */ + var defaultFieldValue = ''; + /** + * Asserts that the field property values are set to default. + * @param {!Blockly.FieldNumber} field The field to check. + */ + var assertFieldDefault = function(field) { + testHelpers.assertFieldValue(field, defaultFieldValue); + }; + /** + * Asserts that the field properties are correct based on the test case. + * @param {!Blockly.FieldNumber} field The field to check. + * @param {!FieldValueTestCase} testCase The test case. + */ + var validTestCaseAssertField = function(field, testCase) { + testHelpers.assertFieldValue(field, testCase.expectedValue); + }; + + testHelpers.runConstructorSuiteTests( + Blockly.FieldTextInput, validValueTestCases, invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + + testHelpers.runFromJsonSuiteTests( + Blockly.FieldTextInput, validValueTestCases,invalidValueTestCases, + validTestCaseAssertField, assertFieldDefault); + suite('setValue', function() { suite('Empty -> New Value', function() { setup(function() { - this.textInputField = new Blockly.FieldTextInput(); + this.field = new Blockly.FieldTextInput(); }); - test('Null', function() { - this.textInputField.setValue(null); - assertValueDefault(this.textInputField); - }); - test('Undefined', function() { - this.textInputField.setValue(undefined); - assertValueDefault(this.textInputField); - }); - test('New String', function() { - this.textInputField.setValue('newValue'); - assertValue(this.textInputField, 'newValue'); - }); - test('Number (Truthy)', function() { - this.textInputField.setValue(1); - assertValue(this.textInputField, '1'); - }); - test('Number (Falsy)', function() { - this.textInputField.setValue(0); - assertValue(this.textInputField, '0'); - }); - test('Boolean True', function() { - this.textInputField.setValue(true); - assertValue(this.textInputField, 'true'); - }); - test('Boolean False', function() { - this.textInputField.setValue(false); - assertValue(this.textInputField, 'false'); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, defaultFieldValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + testHelpers.assertFieldValue(this.field, 'value'); }); }); suite('Value -> New Value', function() { + var initialValue = 'oldValue'; setup(function() { - this.textInputField = new Blockly.FieldTextInput('value'); + this.field = new Blockly.FieldTextInput(initialValue); }); - test('Null', function() { - this.textInputField.setValue(null); - assertValue(this.textInputField, 'value'); - }); - test('Undefined', function() { - this.textInputField.setValue(undefined); - assertValue(this.textInputField, 'value'); - }); - test('New String', function() { - this.textInputField.setValue('newValue'); - assertValue(this.textInputField, 'newValue'); - }); - test('Number (Truthy)', function() { - this.textInputField.setValue(1); - assertValue(this.textInputField, '1'); - }); - test('Number (Falsy)', function() { - this.textInputField.setValue(0); - assertValue(this.textInputField, '0'); - }); - test('Boolean True', function() { - this.textInputField.setValue(true); - assertValue(this.textInputField, 'true'); - }); - test('Boolean False', function() { - this.textInputField.setValue(false); - assertValue(this.textInputField, 'false'); + testHelpers.runSetValueTests( + validValueTestCases, invalidValueTestCases, initialValue); + test('With source block', function() { + this.field.setSourceBlock(createTestBlock()); + this.field.setValue('value'); + testHelpers.assertFieldValue(this.field, 'value'); }); }); }); + suite('Validators', function() { setup(function() { - this.textInputField = new Blockly.FieldTextInput('value'); - this.textInputField.htmlInput_ = Object.create(null); - this.textInputField.htmlInput_.oldValue_ = 'value'; - this.textInputField.htmlInput_.untypedDefaultValue_ = 'value'; - this.stub = sinon.stub(this.textInputField, 'resizeEditor_'); + this.field = new Blockly.FieldTextInput('value'); + this.field.htmlInput_ = Object.create(null); + this.field.htmlInput_.oldValue_ = 'value'; + this.field.htmlInput_.untypedDefaultValue_ = 'value'; + this.stub = sinon.stub(this.field, 'resizeEditor_'); }); teardown(function() { - this.textInputField.setValidator(null); - Blockly.FieldTextInput.htmlInput_ = null; - if (this.stub) { - this.stub.restore(); - } + sinon.restore(); }); - suite('Null Validator', function() { - setup(function() { - this.textInputField.setValidator(function() { - return null; + var testSuites = [ + {title: 'Null Validator', + validator: + function() { + return null; + }, + value: 'newValue', expectedValue: 'value'}, + {title: 'Remove \'a\' Validator', + validator: + function(newValue) { + return newValue.replace(/a/g, ''); + }, + value: 'bbbaaa', expectedValue: 'bbb'}, + {title: 'Returns Undefined Validator', validator: function() {}, + value: 'newValue', expectedValue: 'newValue', expectedText: 'newValue'}, + ]; + testSuites.forEach(function(suiteInfo) { + suite(suiteInfo.title, function() { + setup(function() { + this.field.setValidator(suiteInfo.validator); }); - }); - test('When Editing', function() { - this.textInputField.isBeingEdited_ = true; - this.textInputField.htmlInput_.value = 'newValue'; - this.textInputField.onHtmlInputChange_(null); - assertValue(this.textInputField, 'value', 'newValue'); - this.textInputField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.textInputField.setValue('newValue'); - assertValue(this.textInputField, 'value'); - }); - }); - suite('Remove \'a\' Validator', function() { - setup(function() { - this.textInputField.setValidator(function(newValue) { - return newValue.replace(/a/g, ''); + test('When Editing', function() { + this.field.isBeingEdited_ = true; + this.field.htmlInput_.value = suiteInfo.value; + this.field.onHtmlInputChange_(null); + testHelpers.assertFieldValue( + this.field, suiteInfo.expectedValue, suiteInfo.value); + }); + test('When Not Editing', function() { + this.field.setValue(suiteInfo.value); + testHelpers.assertFieldValue(this.field, suiteInfo.expectedValue); }); - }); - test('When Editing', function() { - this.textInputField.isBeingEdited_ = true; - this.textInputField.htmlInput_.value = 'bbbaaa'; - this.textInputField.onHtmlInputChange_(null); - assertValue(this.textInputField, 'bbb', 'bbbaaa'); - this.textInputField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.textInputField.setValue('bbbaaa'); - assertValue(this.textInputField, 'bbb'); - }); - }); - suite('Returns Undefined Validator', function() { - setup(function() { - this.textInputField.setValidator(function() {}); - }); - test('When Editing', function() { - this.textInputField.isBeingEdited_ = true; - this.textInputField.htmlInput_.value = 'newValue'; - this.textInputField.onHtmlInputChange_(null); - assertValue(this.textInputField, 'newValue'); - this.textInputField.isBeingEdited_ = false; - }); - test('When Not Editing', function() { - this.textInputField.setValue('newValue'); - assertValue(this.textInputField, 'newValue'); }); }); }); + suite('Customization', function() { suite('Spellcheck', function() { setup(function() { diff --git a/tests/mocha/field_variable_test.js b/tests/mocha/field_variable_test.js index c3f6bf3c0..c1d3207a8 100644 --- a/tests/mocha/field_variable_test.js +++ b/tests/mocha/field_variable_test.js @@ -36,8 +36,8 @@ suite('Variable Fields', function() { var actualName = variableField.getText(); var actualId = variableField.getValue(); opt_expectedId = opt_expectedId || FAKE_ID; - assertEquals(actualName, expectedName); - assertEquals(actualId, opt_expectedId); + chai.assert.equal(actualName, expectedName); + chai.assert.equal(actualId, opt_expectedId); } function assertValueDefault(variableField) { assertValue(variableField, FAKE_VARIABLE_NAME, FAKE_ID); @@ -69,10 +69,10 @@ suite('Variable Fields', function() { fieldVariable); // Expect three variable options, a rename option, and a delete option. - assertEquals(result_options.length, 5); - isEqualArrays(result_options[0], ['name1', 'id1']); - isEqualArrays(result_options[1], ['name2', 'id2']); - isEqualArrays(result_options[2], ['name3', 'id3']); + chai.assert.equal(result_options.length, 5); + chai.assert.deepEqual(result_options[0], ['name1', 'id1']); + chai.assert.deepEqual(result_options[1], ['name2', 'id2']); + chai.assert.deepEqual(result_options[2], ['name3', 'id3']); }); suite('Constructor', function() { test('Null', function() { @@ -86,8 +86,8 @@ suite('Variable Fields', function() { }); test('No Value Before InitModel', function() { var fieldVariable = new Blockly.FieldVariable('name1'); - assertEquals('', fieldVariable.getText()); - assertNull(fieldVariable.getValue()); + chai.assert.equal(fieldVariable.getText(), ''); + chai.assert.isNull(fieldVariable.getValue()); }); test('Given Variable Name', function() { var fieldVariable = createAndInitFieldConstructor( @@ -106,8 +106,8 @@ suite('Variable Fields', function() { }); test('No Value Before InitModel', function() { var variableField = new Blockly.FieldVariable('name1'); - assertEquals('', variableField.getText()); - assertNull(variableField.getValue()); + chai.assert.equal(variableField.getText(), ''); + chai.assert.isNull(variableField.getValue()); }); test('Given Variable Name', function() { var variableField = createAndInitFieldJson(this.workspace, 'name1'); @@ -139,8 +139,8 @@ suite('Variable Fields', function() { variableField.setValue('id2'); // Setting value by ID gives us the right text as well. - assertEquals('name2', variableField.getText()); - assertEquals('id2', variableField.getValue()); + chai.assert.equal(variableField.getText(), 'name2'); + chai.assert.equal(variableField.getValue(), 'id2'); chai.assert.notEqual(oldId, variableField.getValue()); }); test('Variable Does not Exist', function() { @@ -244,7 +244,7 @@ suite('Variable Fields', function() { // will be returned (regardless of what types are available on the workspace). var fieldVariable = new Blockly.FieldVariable('name1'); var resultTypes = fieldVariable.getVariableTypes_(); - isEqualArrays(resultTypes, ['']); + chai.assert.deepEqual(resultTypes, ['']); }); test('variableTypes is explicit', function() { // Expect that since variableTypes is defined, it will be the return @@ -252,9 +252,9 @@ suite('Variable Fields', function() { var fieldVariable = new Blockly.FieldVariable( 'name1', null, ['type1', 'type2'], 'type1'); var resultTypes = fieldVariable.getVariableTypes_(); - isEqualArrays(resultTypes, ['type1', 'type2']); - assertEquals('Default type was wrong', 'type1', - fieldVariable.defaultType_); + chai.assert.deepEqual(resultTypes, ['type1', 'type2']); + chai.assert.equal(fieldVariable.defaultType_, 'type1', + 'Default type was wrong'); }); test('variableTypes is null', function() { // Expect all variable types to be returned. @@ -267,7 +267,7 @@ suite('Variable Fields', function() { var resultTypes = fieldVariable.getVariableTypes_(); // The empty string is always one of the options. - isEqualArrays(resultTypes, ['type1', 'type2', '']); + chai.assert.deepEqual(resultTypes, ['type1', 'type2', '']); }); test('variableTypes is the empty list', function() { var fieldVariable = new Blockly.FieldVariable('name1'); @@ -283,15 +283,14 @@ suite('Variable Fields', function() { suite('Default types', function() { test('Default type exists', function() { var fieldVariable = new Blockly.FieldVariable(null, null, ['b'], 'b'); - assertEquals('The variable field\'s default type should be "b"', - 'b', fieldVariable.defaultType_); + chai.assert.equal(fieldVariable.defaultType_, 'b', + 'The variable field\'s default type should be "b"'); }); test('No default type', function() { var fieldVariable = new Blockly.FieldVariable(null); - assertEquals('The variable field\'s default type should be the empty string', - '', fieldVariable.defaultType_); - assertNull('The variable field\'s allowed types should be null', - fieldVariable.variableTypes); + chai.assert.equal(fieldVariable.defaultType_, '', 'The variable field\'s default type should be the empty string'); + chai.assert.isNull(fieldVariable.variableTypes, + 'The variable field\'s allowed types should be null'); }); test('Default type mismatch', function() { // Invalid default type when creating a variable field. diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js new file mode 100644 index 000000000..1a73f095b --- /dev/null +++ b/tests/mocha/flyout_test.js @@ -0,0 +1,105 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +suite('Flyout', function() { + setup(function() { + Blockly.defineBlocksWithJsonArray([{ + "type": "basic_block", + "message0": "%1", + "args0": [ + { + "type": "field_input", + "name": "TEXT", + "text": "default" + } + ] + }]); + this.toolboxXml = document.getElementById('toolbox-simple'); + this.workspace = Blockly.inject('blocklyDiv', + { + toolbox: this.toolboxXml + }); + }); + + teardown(function() { + this.workspace.dispose(); + delete Blockly.Blocks['basic_block']; + sinon.restore(); + }); + + suite('createFlyoutInfo_', function() { + setup(function() { + this.simpleToolboxJSON = getSimpleJSON(); + this.flyout = this.workspace.getFlyout(); + this.createFlyoutSpy = sinon.spy(this.flyout, 'createFlyoutInfo_'); + + }); + + function checkLayoutContents(actual, expected, opt_message) { + chai.assert.equal(actual.length, expected.length, opt_message); + for (var i = 0; i < actual.length; i++) { + chai.assert.equal(actual[i].type, expected[i].type, opt_message); + if (actual[i].type == 'BLOCK') { + chai.assert.typeOf(actual[i]['block'], 'Blockly.Block'); + } else if (actual[i].type == 'BUTTON' || actual[i].type == 'LABEL') { + chai.assert.typeOf(actual[i]['block'], 'Blockly.FlyoutButton'); + } + } + } + + function checkFlyoutInfo(flyoutSpy) { + var expectedContents = [ + {type: "block"}, + {type: "button"}, + {type: "button"} + ]; + var expectedGaps = [20,24,24]; + var flyoutInfo = flyoutSpy.returnValues[0]; + var contents = flyoutInfo.contents; + var gaps = flyoutInfo.gaps; + chai.assert.deepEqual(gaps, expectedGaps); + checkLayoutContents(contents, expectedContents, 'Contents'); + } + + test('Node', function() { + this.flyout.show(this.toolboxXml); + checkFlyoutInfo(this.createFlyoutSpy); + }); + test('NodeList', function() { + var nodeList = document.getElementById('toolbox-simple').childNodes; + this.flyout.show(nodeList); + checkFlyoutInfo(this.createFlyoutSpy); + }); + test('Array of JSON', function() { + this.flyout.show(this.simpleToolboxJSON); + checkFlyoutInfo(this.createFlyoutSpy); + }); + test('Array of xml', function() { + this.flyout.show(getXmlArray()); + checkFlyoutInfo(this.createFlyoutSpy); + }); + test('Custom Toolbox: No Category Available', function() { + chai.assert.throws(function() { + this.flyout.show('someString'); + }.bind(this), 'Couldn\'t find a callback function when opening' + + ' a toolbox category.'); + }); + test('Custom Toolbox: Function does not return array', function() { + sinon.stub(this.flyout.workspace_.targetWorkspace, + 'getToolboxCategoryCallback').returns(function(){return null;}); + chai.assert.throws(function() { + this.flyout.show('someString'); + }.bind(this), 'Result of toolbox category callback must be an array.'); + }); + test('Custom Toolbox: Returns Array', function() { + sinon.stub(this.flyout.workspace_.targetWorkspace, + 'getToolboxCategoryCallback').returns(function(){return getXmlArray();}); + chai.assert.doesNotThrow(function() { + this.flyout.show('someString'); + }.bind(this)); + }); + }); +}); diff --git a/tests/mocha/generator_test.js b/tests/mocha/generator_test.js new file mode 100644 index 000000000..cf91990ea --- /dev/null +++ b/tests/mocha/generator_test.js @@ -0,0 +1,158 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.require('Blockly.Dart'); +goog.require('Blockly.JavaScript'); +goog.require('Blockly.Lua'); +goog.require('Blockly.PHP'); +goog.require('Blockly.Python'); + +suite('Generator', function() { + setup(function() { + this.workspace = new Blockly.Workspace(); + }); + + teardown(function() { + this.workspace.dispose(); + }); + + suite('prefix', function() { + setup(function() { + this.generator = new Blockly.Generator('INTERCAL'); + }); + + test('Nothing', function() { + chai.assert.equal(this.generator.prefixLines('', ''), ''); + }); + + test('One word', function() { + chai.assert.equal(this.generator.prefixLines('Hello', '@'), '@Hello') ; + }); + + test('One line', function() { + chai.assert.equal(this.generator.prefixLines('Hello\n', '12'), '12Hello\n'); + }); + + test('Two lines', function() { + chai.assert.equal(this.generator.prefixLines('Hello\nWorld\n', '***'), '***Hello\n***World\n'); + }); + }); + + suite('blockToCode', function() { + setup(function() { + Blockly.defineBlocksWithJsonArray([{ + "type": "stack_block", + "message0": "", + "previousStatement": null, + "nextStatement": null + }, + { + "type": "row_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "output": null, + "nextStatement": null + }]); + var rowBlock = this.workspace.newBlock('row_block'); + var stackBlock = this.workspace.newBlock('stack_block'); + + this.blockToCodeTest = function( + generator, blockDisabled, opt_thisOnly, + expectedCode, opt_message) { + generator.row_block = function(_){return 'row_block';}; + generator.stack_block = function(_){return 'stack_block';}; + rowBlock.nextConnection.connect(stackBlock.previousConnection); + rowBlock.disabled = blockDisabled; + + var code = generator.blockToCode(rowBlock, opt_thisOnly); + chai.assert.equal(code, expectedCode, opt_message); + }; + }); + + teardown(function() { + delete Blockly.Blocks['stack_block']; + delete Blockly.Blocks['row_block']; + }); + + var testCase = [ + [Blockly.Dart, 'Dart'], + [Blockly.JavaScript, 'JavaScript'], + [Blockly.Lua, 'Lua'], + [Blockly.PHP, 'PHP'], + [Blockly.Python, 'Python']]; + + suite('Trivial', function() { + testCase.forEach(function(testCase) { + var generator = testCase[0]; + var name = testCase[1]; + test(name, function() { + this.blockToCodeTest(generator, false, true, 'row_block'); + this.blockToCodeTest( + generator, false, false, 'row_blockstack_block', 'thisOnly=false'); + }); + }); + }); + + suite('Disabled block', function() { + testCase.forEach(function(testCase) { + var generator = testCase[0]; + var name = testCase[1]; + test(name, function() { + this.blockToCodeTest(generator, true, true, ''); + this.blockToCodeTest(generator, true, false, 'stack_block', 'thisOnly=false'); + }); + }); + }); + + suite('Nested block', function() { + setup(function() { + Blockly.defineBlocksWithJsonArray([ { + "type": "test_loop_block", + "message0": "Repeat Loop", + "message1": "%1", + "args1": [{ + "type": "input_statement", + "name": "DO" + }], + "previousStatement": null, + "nextStatement": null + }]); + var blockA = this.workspace.newBlock('test_loop_block'); + var blockB = this.workspace.newBlock('test_loop_block'); + var blockC = this.workspace.newBlock('test_loop_block'); + this.loopTest = function( + generator, opt_thisOnly, expectedCode, opt_message) { + generator.test_loop_block = function(block){ + return '{' + generator.statementToCode(block, 'DO') + '}'; + }; + blockA.getInput('DO').connection.connect(blockB.previousConnection); + blockA.nextConnection.connect(blockC.previousConnection); + + var code = generator.blockToCode(blockA, opt_thisOnly); + chai.assert.equal(code, expectedCode, opt_message); + }; + }); + + teardown(function() { + delete Blockly.Blocks['test_loop_block']; + }); + + testCase.forEach(function(testCase) { + var generator = testCase[0]; + var name = testCase[1]; + test(name, function() { + this.loopTest(generator, true, '{ {}}'); + this.loopTest(generator, false, '{ {}}{}', 'thisOnly=false'); + }); + }); + }); + }); +}); diff --git a/tests/mocha/gesture_test.js b/tests/mocha/gesture_test.js index 974c7ec0e..89fab0323 100644 --- a/tests/mocha/gesture_test.js +++ b/tests/mocha/gesture_test.js @@ -24,8 +24,8 @@ suite('Gesture', function() { test('Constructor', function() { var gesture = new Blockly.Gesture(this.e, this.workspace); - assertEquals(gesture.mostRecentEvent_, this.e); - assertEquals(gesture.creatorWorkspace_, this.workspace); + chai.assert.equal(gesture.mostRecentEvent_, this.e); + chai.assert.equal(gesture.creatorWorkspace_, this.workspace); }); test('Field click - Click in workspace', function() { @@ -37,7 +37,7 @@ suite('Gesture', function() { gesture.setStartField(field); var isFieldClick = gesture.isFieldClick_(); - assertEquals(isFieldClick, true); + chai.assert.isTrue(isFieldClick); }); function gestureIsFieldClick_InFlyoutHelper(flyout, expectedResult){ @@ -55,7 +55,7 @@ suite('Gesture', function() { gesture.setStartFlyout_(this.workspace.flyout_); var isFieldClick = gesture.isFieldClick_(); - assertEquals(isFieldClick, expectedResult); + chai.assert.equal(isFieldClick, expectedResult); } test('Field click - Auto close flyout', function() { @@ -82,6 +82,6 @@ suite('Gesture', function() { var gesture = new Blockly.Gesture(event, ws); gesture.doWorkspaceClick_(event); var cursor = ws.getCursor(); - assertEquals(cursor.getCurNode().getType(), Blockly.ASTNode.types.WORKSPACE); + chai.assert.equal(cursor.getCurNode().getType(), Blockly.ASTNode.types.WORKSPACE); }); }); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index f535ba2e1..bc236ad93 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -6,8 +6,18 @@ + + + + + + + + + + @@ -16,6 +26,7 @@ + + @@ -33,11 +45,11 @@ + - @@ -46,24 +58,56 @@ + + + + + + + + + + + + + +
+ + + + +