diff --git a/.eslintrc.json b/.eslintrc.json index e474d8566..63bfb43b7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -61,8 +61,6 @@ "space-infix-ops": ["error"], "_comment": "Blockly uses 'use strict' in files", "strict": ["off"], - "_comment": "Blockly often uses cond-assignment in loops", - "no-cond-assign": ["off"], "_comment": "Closure style allows redeclarations", "no-redeclare": ["off"], "valid-jsdoc": ["error", {"requireReturn": false}], @@ -74,7 +72,8 @@ "balanced": true }, "exceptions": ["*"] - }] + }], + "es5/no-es6-methods": ["warn"] }, "env": { "browser": true @@ -83,5 +82,8 @@ "Blockly": true, "goog": true }, - "extends": "eslint:recommended" + "extends": [ + "eslint:recommended", + "plugin:es5/no-es2015" + ] } diff --git a/.travis.yml b/.travis.yml index b5f109f00..643c96a63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ dist: xenial node_js: - 8 - 10 + - 12 addons: chrome: stable firefox: latest diff --git a/blockly_compressed.js b/blockly_compressed.js index 9de0658d8..d0be7881f 100644 --- a/blockly_compressed.js +++ b/blockly_compressed.js @@ -1,49 +1,124 @@ -// Do not edit this file; automatically generated by build.py. -'use strict'; - - -var Blockly={};Blockly.Blocks=Object.create(null); -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.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,d=a.y-b.y;return Math.sqrt(c*c+d*d)};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)}; +// 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.INSERTION_MARKER_COLOUR="#000000";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, +c):null};Blockly.utils.colour.rgbToHex=function(a,b,c){b=a<<16|b<<8|c;return 16>a?"#"+(16777216|b).toString(16).substr(1):"#"+b.toString(16)};Blockly.utils.colour.hexToRgb=function(a){a=Blockly.utils.colour.parse(a);if(!a)return[0,0,0];a=parseInt(a.substr(1),16);return[a>>16,a>>8&255,a&255]}; +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.lengthb&&(b=c[d].length);d=-Infinity;var e=1;do{var f=d;var g=a;var h=[],k=c.length/e,l=1;for(d=0;df);return g}; +Blockly.utils.string.commonWordPrefix=function(a,b){if(!a.length)return 0;if(1==a.length)return a[0].length;var c=0;b=b||Blockly.utils.string.shortestStringLength(a);for(var d=0;db&&(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}; Blockly.utils.string.wrapScore_=function(a,b,c){for(var d=[0],e=[],f=0;fd&&(d=h,e=g)}return e?Blockly.utils.string.wrapMutate_(a,e,c):b};Blockly.utils.string.wrapToText_=function(a,b){for(var c=[],d=0;d=k?(e=2,g=k,(k=f.join(""))&&c.push(k),f.length=0):"{"==k?e=3:(f.push("%",k),e=0):2==e?"0"<=k&&"9">=k?g+=k:(c.push(parseInt(g,10)),h--,e=0):3==e&&(""==k?(f.splice(0,0,"%{"),h--,e=0):"}"!=k?f.push(k):(e=f.join(""),/[A-Z]\w*/i.test(e)?(k=e.toUpperCase(),(k= -Blockly.utils.string.startsWith(k,"BKY_")?k.substring(4):null)&&k in Blockly.Msg?(e=Blockly.Msg[k],"string"==typeof e?Array.prototype.push.apply(c,Blockly.utils.tokenizeInterpolation_(e,b)):b?c.push(String(e)):c.push(e)):c.push("%{"+e+"}")):c.push("%{"+e+"}"),e=f.length=0))}(k=f.join(""))&&c.push(k);d=[];for(h=f.length=0;h=h?(e=2,f=h,(h=a.join(""))&&c.push(h),a.length=0):"{"==h?e=3:(a.push("%",h),e=0):2==e?"0"<=h&&"9">=h?f+=h:(c.push(parseInt(f,10)),g--,e=0):3==e&&(""==h?(a.splice(0,0,"%{"),g--,e=0):"}"!=h?a.push(h):(e=a.join(""),/[A-Z]\w*/i.test(e)?(h=e.toUpperCase(), +(h=Blockly.utils.string.startsWith(h,"BKY_")?h.substring(4):null)&&h in Blockly.Msg?(e=Blockly.Msg[h],"string"==typeof e?Array.prototype.push.apply(c,Blockly.utils.tokenizeInterpolation_(e,b)):b?c.push(String(e)):c.push(e)):c.push("%{"+e+"}")):c.push("%{"+e+"}"),e=a.length=0))}(h=a.join(""))&&c.push(h);b=[];for(g=a.length=0;gc;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){var c=a.indexOf(b);if(-1==c)return!1;a.splice(c,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);if(b){var e=a.getNextBlock();e&&(e=d.indexOf(e),d.splice(e,d.length-e))}e=0;for(var f;f=d[e];e++)c[f.type]?c[f.type]++:c[f.type]=1;return c};Blockly.utils.screenToWsCoordinates=function(a,b){var c=b.x,d=b.y,e=a.getInjectionDiv().getBoundingClientRect();c=new Blockly.utils.Coordinate(c-e.left,d-e.top);d=a.getOriginOffsetInPixels();return Blockly.utils.Coordinate.difference(c,d).scale(1/a.scale)}; -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"; +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.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"; Blockly.Events.VAR_RENAME="var_rename";Blockly.Events.UI="ui";Blockly.Events.COMMENT_CREATE="comment_create";Blockly.Events.COMMENT_DELETE="comment_delete";Blockly.Events.COMMENT_CHANGE="comment_change";Blockly.Events.COMMENT_MOVE="comment_move";Blockly.Events.FINISHED_LOADING="finished_loading";Blockly.Events.BUMP_EVENTS=[Blockly.Events.BLOCK_CREATE,Blockly.Events.BLOCK_MOVE,Blockly.Events.COMMENT_CREATE,Blockly.Events.COMMENT_MOVE];Blockly.Events.FIRE_QUEUE_=[]; Blockly.Events.fire=function(a){Blockly.Events.isEnabled()&&(Blockly.Events.FIRE_QUEUE_.length||setTimeout(Blockly.Events.fireNow_,0),Blockly.Events.FIRE_QUEUE_.push(a))};Blockly.Events.fireNow_=function(){for(var a=Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_,!0),b=Blockly.Events.FIRE_QUEUE_.length=0,c;c=a[b];b++)if(c.workspaceId){var d=Blockly.Workspace.getById(c.workspaceId);d&&d.fireChangeListener(c)}}; -Blockly.Events.filter=function(a,b){var c=a.slice();b||c.reverse();for(var d=[],e=Object.create(null),f=0,g;g=c[f];f++)if(!g.isNull()){var h=[g.type,g.blockId,g.workspaceId].join(" "),k=e[h],l=k?k.event:null;if(!k)e[h]={event:g,index:f},d.push(g);else if(g.type==Blockly.Events.MOVE&&k.index==f-1)l.newParentId=g.newParentId,l.newInputName=g.newInputName,l.newCoordinate=g.newCoordinate,k.index=f;else if(g.type==Blockly.Events.CHANGE&&g.element==l.element&&g.name==l.name)l.newValue=g.newValue;else if(g.type!= -Blockly.Events.UI||"click"!=g.element||"commentOpen"!=l.element&&"mutatorOpen"!=l.element&&"warningOpen"!=l.element)e[h]={event:g,index:1},d.push(g)}c=d.filter(function(a){return!a.isNull()});b||c.reverse();for(f=1;g=c[f];f++)g.type==Blockly.Events.CHANGE&&"mutation"==g.element&&c.unshift(c.splice(f,1)[0]);return c};Blockly.Events.clearPendingUndo=function(){for(var a=0,b;b=Blockly.Events.FIRE_QUEUE_[a];a++)b.recordUndo=!1};Blockly.Events.disable=function(){Blockly.Events.disabled_++}; +Blockly.Events.filter=function(a,b){a=a.slice();b||a.reverse();for(var c=[],d=Object.create(null),e=0,f;f=a[e];e++)if(!f.isNull()){var g=[f.type,f.blockId,f.workspaceId].join(" "),h=d[g],k=h?h.event:null;if(!h)d[g]={event:f,index:e},c.push(f);else if(f.type==Blockly.Events.MOVE&&h.index==e-1)k.newParentId=f.newParentId,k.newInputName=f.newInputName,k.newCoordinate=f.newCoordinate,h.index=e;else if(f.type==Blockly.Events.CHANGE&&f.element==k.element&&f.name==k.name)k.newValue=f.newValue;else if(f.type!= +Blockly.Events.UI||"click"!=f.element||"commentOpen"!=k.element&&"mutatorOpen"!=k.element&&"warningOpen"!=k.element)d[g]={event:f,index:1},c.push(f)}a=c.filter(function(a){return!a.isNull()});b||a.reverse();for(e=1;f=a[e];e++)f.type==Blockly.Events.CHANGE&&"mutation"==f.element&&a.unshift(a.splice(e,1)[0]);return a};Blockly.Events.clearPendingUndo=function(){for(var a=0,b;b=Blockly.Events.FIRE_QUEUE_[a];a++)b.recordUndo=!1};Blockly.Events.disable=function(){Blockly.Events.disabled_++}; Blockly.Events.enable=function(){Blockly.Events.disabled_--};Blockly.Events.isEnabled=function(){return 0==Blockly.Events.disabled_};Blockly.Events.getGroup=function(){return Blockly.Events.group_};Blockly.Events.setGroup=function(a){Blockly.Events.group_="boolean"==typeof a?a?Blockly.utils.genUid():"":a};Blockly.Events.getDescendantIds=function(a){var b=[];a=a.getDescendants(!1);for(var c=0,d;d=a[c];c++)b[c]=d.id;return b}; Blockly.Events.fromJson=function(a,b){switch(a.type){case Blockly.Events.CREATE:var c=new Blockly.Events.Create(null);break;case Blockly.Events.DELETE:c=new Blockly.Events.Delete(null);break;case Blockly.Events.CHANGE:c=new Blockly.Events.Change(null,"","","","");break;case Blockly.Events.MOVE:c=new Blockly.Events.Move(null);break;case Blockly.Events.VAR_CREATE:c=new Blockly.Events.VarCreate(null);break;case Blockly.Events.VAR_DELETE:c=new Blockly.Events.VarDelete(null);break;case Blockly.Events.VAR_RENAME:c= -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;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.values=function(a){return Object.values?Object.values(a):Object.keys(a).map(function(b){return a[b]})};Blockly.utils.xml={};Blockly.utils.xml.NAME_SPACE="https://developers.google.com/blockly/xml";Blockly.utils.xml.document=function(){return document};Blockly.utils.xml.createElement=function(a){return Blockly.utils.xml.document().createElementNS(Blockly.utils.xml.NAME_SPACE,a)};Blockly.utils.xml.createTextNode=function(a){return Blockly.utils.xml.document().createTextNode(a)};Blockly.utils.xml.textToDomDocument=function(a){return(new DOMParser).parseFromString(a,"text/xml")}; +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.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.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")}; +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 {", +"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;","}",".blocklyBubbleText {","fill: #000;","}",".blocklyFlyout {","position: absolute;","z-index: 20;","}",".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);","}",".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(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.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_=null);Blockly.DropDownDiv.themeClassName_&&(Blockly.utils.dom.removeClass(a,Blockly.DropDownDiv.themeClassName_),Blockly.DropDownDiv.themeClassName_=null);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}; +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};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();var c=Blockly.Xml.domToWorkspace(a,b);b.setResizesEnabled(!0);return c}; -Blockly.Xml.domToWorkspace=function(a,b){if(a instanceof Blockly.Workspace){var c=a;a=b;b=c;console.warn("Deprecated call to Blockly.Xml.domToWorkspace, swap the arguments.")}var d;b.RTL&&(d=b.getWidth());c=[];Blockly.utils.dom.startTextWidthCache();var e=a.childNodes.length,f=Blockly.Events.getGroup();f||Blockly.Events.setGroup(!0);b.setResizesEnabled&&b.setResizesEnabled(!1);var g=!0;try{for(var h=0;hg&&(g=k.x)}d=d-h+10;e=b.RTL?e-g:e-f;for(c=0;cc)){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;1c-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;e 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.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")}; @@ -178,7 +180,7 @@ Blockly.Touch.splitEventByTouches=function(a){var b=[];if(a.changedTouches)for(v Blockly.ScrollbarPair.prototype.oldHostMetrics_=null;Blockly.ScrollbarPair.prototype.dispose=function(){Blockly.utils.dom.removeNode(this.corner_);this.oldHostMetrics_=this.workspace_=this.corner_=null;this.hScroll.dispose();this.hScroll=null;this.vScroll.dispose();this.vScroll=null}; Blockly.ScrollbarPair.prototype.resize=function(){var a=this.workspace_.getMetrics();if(a){var b=!1,c=!1;this.oldHostMetrics_&&this.oldHostMetrics_.viewWidth==a.viewWidth&&this.oldHostMetrics_.viewHeight==a.viewHeight&&this.oldHostMetrics_.absoluteTop==a.absoluteTop&&this.oldHostMetrics_.absoluteLeft==a.absoluteLeft?(this.oldHostMetrics_&&this.oldHostMetrics_.contentWidth==a.contentWidth&&this.oldHostMetrics_.viewLeft==a.viewLeft&&this.oldHostMetrics_.contentLeft==a.contentLeft||(b=!0),this.oldHostMetrics_&& this.oldHostMetrics_.contentHeight==a.contentHeight&&this.oldHostMetrics_.viewTop==a.viewTop&&this.oldHostMetrics_.contentTop==a.contentTop||(c=!0)):c=b=!0;b&&this.hScroll.resize(a);c&&this.vScroll.resize(a);this.oldHostMetrics_&&this.oldHostMetrics_.viewWidth==a.viewWidth&&this.oldHostMetrics_.absoluteLeft==a.absoluteLeft||this.corner_.setAttribute("x",this.vScroll.position_.x);this.oldHostMetrics_&&this.oldHostMetrics_.viewHeight==a.viewHeight&&this.oldHostMetrics_.absoluteTop==a.absoluteTop||this.corner_.setAttribute("y", -this.hScroll.position_.y);this.oldHostMetrics_=a}};Blockly.ScrollbarPair.prototype.set=function(a,b){var c={},d=a*this.hScroll.ratio_,e=b*this.vScroll.ratio_,f=this.vScroll.scrollViewSize_;c.x=this.getRatio_(d,this.hScroll.scrollViewSize_);c.y=this.getRatio_(e,f);this.workspace_.setMetrics(c);this.hScroll.setHandlePosition(d);this.vScroll.setHandlePosition(e)};Blockly.ScrollbarPair.prototype.getRatio_=function(a,b){var c=a/b;return isNaN(c)?0:c}; +this.hScroll.position_.y);this.oldHostMetrics_=a}};Blockly.ScrollbarPair.prototype.set=function(a,b){var c={};a*=this.hScroll.ratio_;b*=this.vScroll.ratio_;var d=this.vScroll.scrollViewSize_;c.x=this.getRatio_(a,this.hScroll.scrollViewSize_);c.y=this.getRatio_(b,d);this.workspace_.setMetrics(c);this.hScroll.setHandlePosition(a);this.vScroll.setHandlePosition(b)};Blockly.ScrollbarPair.prototype.getRatio_=function(a,b){a/=b;return isNaN(a)?0:a}; Blockly.Scrollbar=function(a,b,c,d){this.workspace_=a;this.pair_=c||!1;this.horizontal_=b;this.oldHostMetrics_=null;this.createDom_(d);this.position_=new Blockly.utils.Coordinate(0,0);a=Blockly.Scrollbar.scrollbarThickness;b?(this.svgBackground_.setAttribute("height",a),this.outerSvg_.setAttribute("height",a),this.svgHandle_.setAttribute("height",a-5),this.svgHandle_.setAttribute("y",2.5),this.lengthAttribute_="width",this.positionAttribute_="x"):(this.svgBackground_.setAttribute("width",a),this.outerSvg_.setAttribute("width", a),this.svgHandle_.setAttribute("width",a-5),this.svgHandle_.setAttribute("x",2.5),this.lengthAttribute_="height",this.positionAttribute_="y");this.onMouseDownBarWrapper_=Blockly.bindEventWithChecks_(this.svgBackground_,"mousedown",this,this.onMouseDownBar_);this.onMouseDownHandleWrapper_=Blockly.bindEventWithChecks_(this.svgHandle_,"mousedown",this,this.onMouseDownHandle_)};Blockly.Scrollbar.prototype.origin_=new Blockly.utils.Coordinate(0,0);Blockly.Scrollbar.prototype.startDragMouse_=0; Blockly.Scrollbar.prototype.scrollViewSize_=0;Blockly.Scrollbar.prototype.handleLength_=0;Blockly.Scrollbar.prototype.handlePosition_=0;Blockly.Scrollbar.prototype.isVisible_=!0;Blockly.Scrollbar.prototype.containerVisible_=!0;Blockly.Scrollbar.scrollbarThickness=15;Blockly.Touch.TOUCH_ENABLED&&(Blockly.Scrollbar.scrollbarThickness=25); @@ -192,7 +194,7 @@ Blockly.Scrollbar.prototype.resizeContentHorizontal=function(a){this.pair_||this Blockly.Scrollbar.prototype.resizeViewVertical=function(a){var b=a.viewHeight-1;this.pair_&&(b-=Blockly.Scrollbar.scrollbarThickness);this.setScrollViewSize_(Math.max(0,b));b=a.absoluteLeft+.5;this.workspace_.RTL||(b+=a.viewWidth-Blockly.Scrollbar.scrollbarThickness-1);this.setPosition_(b,a.absoluteTop+.5);this.resizeContentVertical(a)}; Blockly.Scrollbar.prototype.resizeContentVertical=function(a){this.pair_||this.setVisible(this.scrollViewSize_=c+this.handleLength_&&(d+= @@ -201,89 +203,7 @@ Blockly.Scrollbar.prototype.onMouseDownHandle_=function(a){this.workspace_.markF a.stopPropagation(),a.preventDefault())};Blockly.Scrollbar.prototype.onMouseMoveHandle_=function(a){this.setHandlePosition(this.constrainHandle_(this.startDragHandle+((this.horizontal_?a.clientX:a.clientY)-this.startDragMouse_)));this.onScroll_()};Blockly.Scrollbar.prototype.onMouseUpHandle_=function(){this.workspace_.resetDragSurface();Blockly.Touch.clearTouchIdentifier();this.cleanUp_()}; Blockly.Scrollbar.prototype.cleanUp_=function(){Blockly.hideChaff(!0);Blockly.Scrollbar.onMouseUpWrapper_&&(Blockly.unbindEvent_(Blockly.Scrollbar.onMouseUpWrapper_),Blockly.Scrollbar.onMouseUpWrapper_=null);Blockly.Scrollbar.onMouseMoveWrapper_&&(Blockly.unbindEvent_(Blockly.Scrollbar.onMouseMoveWrapper_),Blockly.Scrollbar.onMouseMoveWrapper_=null)}; Blockly.Scrollbar.prototype.constrainHandle_=function(a){return a=0>=a||isNaN(a)||this.scrollViewSize_a)throw Error("Cannot unsubscribe a workspace that hasn't been subscribed.");this.subscribedWorkspaces_.splice(a,1)};Blockly.ThemeManager.prototype.subscribe=function(a,b,c){this.componentDB_[b]||(this.componentDB_[b]=[]);this.componentDB_[b].push({element:a,propertyName:c});b=this.theme_&&this.theme_.getComponentStyle(b);a.style[c]=b||""}; -Blockly.ThemeManager.prototype.unsubscribe=function(a){if(a)for(var b=Object.keys(this.componentDB_),c=0,d;d=b[c];c++){for(var e=this.componentDB_[d],f=e.length-1;0<=f;f--)e[f].element===a&&e.splice(f,1);this.componentDB_[d].length||delete this.componentDB_[d]}};Blockly.ThemeManager.prototype.dispose=function(){this.componentDB_=this.subscribedWorkspaces_=this.theme_=this.owner_=null};Blockly.Themes={};Blockly.Themes.Classic={};Blockly.Themes.Classic.defaultBlockStyles={colour_blocks:{colourPrimary:"20"},list_blocks:{colourPrimary:"260"},logic_blocks:{colourPrimary:"210"},loop_blocks:{colourPrimary:"120"},math_blocks:{colourPrimary:"230"},procedure_blocks:{colourPrimary:"290"},text_blocks:{colourPrimary:"160"},variable_blocks:{colourPrimary:"330"},variable_dynamic_blocks:{colourPrimary:"310"},hat_blocks:{colourPrimary:"330",hat:"cap"}}; -Blockly.Themes.Classic.categoryStyles={colour_category:{colour:"20"},list_category:{colour:"260"},logic_category:{colour:"210"},loop_category:{colour:"120"},math_category:{colour:"230"},procedure_category:{colour:"290"},text_category:{colour:"160"},variable_category:{colour:"330"},variable_dynamic_category:{colour:"310"}};Blockly.Themes.Classic=new Blockly.Theme(Blockly.Themes.Classic.defaultBlockStyles,Blockly.Themes.Classic.categoryStyles);Blockly.VariableMap=function(a){this.variableMap_=Object.create(null);this.workspace=a};Blockly.VariableMap.prototype.clear=function(){this.variableMap_=Object.create(null)};Blockly.VariableMap.prototype.renameVariable=function(a,b){var c=this.getVariable(b,a.type),d=this.workspace.getAllBlocks(!1);Blockly.Events.setGroup(!0);try{c&&c.getId()!=a.getId()?this.renameVariableWithConflict_(a,b,c,d):this.renameVariableAndUses_(a,b,d)}finally{Blockly.Events.setGroup(!1)}}; -Blockly.VariableMap.prototype.renameVariableById=function(a,b){var c=this.getVariableById(a);if(!c)throw Error("Tried to rename a variable that didn't exist. ID: "+a);this.renameVariable(c,b)};Blockly.VariableMap.prototype.renameVariableAndUses_=function(a,b,c){Blockly.Events.fire(new Blockly.Events.VarRename(a,b));a.name=b;for(b=0;bthis.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.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.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.Workspace.prototype.getThemeManager=function(){return this.themeManager_};Blockly.Bubble=function(a,b,c,d,e,f){this.workspace_=a;this.content_=b;this.shape_=c;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||(b=this.content_.getBBox(),e=b.width+2*Blockly.Bubble.BORDER_WIDTH,f=b.height+2*Blockly.Bubble.BORDER_WIDTH);this.setBubbleSize(e,f);this.positionBubble_();this.renderArrow_();this.rendered_=!0;a.options.readOnly|| -(Blockly.bindEventWithChecks_(this.bubbleBack_,"mousedown",this,this.bubbleMouseDown_),this.resizeGroup_&&Blockly.bindEventWithChecks_(this.resizeGroup_,"mousedown",this,this.resizeMouseDown_))};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.prototype.resizeCallback_=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(){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; -Blockly.Bubble.prototype.createDom_=function(a,b){this.bubbleGroup_=Blockly.utils.dom.createSvgElement("g",{},null);var c={filter:"url(#"+this.workspace_.options.embossFilterId+")"};Blockly.utils.userAgent.JAVA_FX&&(c={});c=Blockly.utils.dom.createSvgElement("g",c,this.bubbleGroup_);this.bubbleArrow_=Blockly.utils.dom.createSvgElement("path",{},c);this.bubbleBack_=Blockly.utils.dom.createSvgElement("rect",{"class":"blocklyDraggable",x:0,y:0,rx:Blockly.Bubble.BORDER_WIDTH,ry:Blockly.Bubble.BORDER_WIDTH}, -c);b?(this.resizeGroup_=Blockly.utils.dom.createSvgElement("g",{"class":this.workspace_.RTL?"blocklyResizeSW":"blocklyResizeSE"},this.bubbleGroup_),c=2*Blockly.Bubble.BORDER_WIDTH,Blockly.utils.dom.createSvgElement("polygon",{points:"0,x x,x x,0".replace(/x/g,c.toString())},this.resizeGroup_),Blockly.utils.dom.createSvgElement("line",{"class":"blocklyResizeLine",x1:c/3,y1:c-1,x2:c-1,y2:c/3},this.resizeGroup_),Blockly.utils.dom.createSvgElement("line",{"class":"blocklyResizeLine",x1:2*c/3,y1:c-1,x2:c- -1,y2:2*c/3},this.resizeGroup_)):this.resizeGroup_=null;this.bubbleGroup_.appendChild(a);return this.bubbleGroup_};Blockly.Bubble.prototype.getSvgRoot=function(){return this.bubbleGroup_};Blockly.Bubble.prototype.setSvgId=function(a){this.bubbleGroup_.dataset&&(this.bubbleGroup_.dataset.blockId=a)};Blockly.Bubble.prototype.bubbleMouseDown_=function(a){var b=this.workspace_.getGesture(a);b&&b.handleBubbleStart(a,this)};Blockly.Bubble.prototype.showContextMenu_=function(a){}; -Blockly.Bubble.prototype.isDeletable=function(){return!1}; -Blockly.Bubble.prototype.resizeMouseDown_=function(a){this.promote_();Blockly.Bubble.unbindDragEvents_();Blockly.utils.isRightButton(a)||(this.workspace_.startDrag(a,new Blockly.utils.Coordinate(this.workspace_.RTL?-this.width_:this.width_,this.height_)),Blockly.Bubble.onMouseUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",this,Blockly.Bubble.bubbleMouseUp_),Blockly.Bubble.onMouseMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",this,this.resizeMouseMove_),Blockly.hideChaff()); -a.stopPropagation()};Blockly.Bubble.prototype.resizeMouseMove_=function(a){this.autoLayout_=!1;a=this.workspace_.moveDrag(a);this.setBubbleSize(this.workspace_.RTL?-a.x:a.x,a.y);this.workspace_.RTL&&this.positionBubble_()};Blockly.Bubble.prototype.registerResizeEvent=function(a){this.resizeCallback_=a};Blockly.Bubble.prototype.promote_=function(){var a=this.bubbleGroup_.parentNode;return a.lastChild!==this.bubbleGroup_?(a.appendChild(this.bubbleGroup_),!0):!1}; -Blockly.Bubble.prototype.setAnchorLocation=function(a){this.anchorXY_=a;this.rendered_&&this.positionBubble_()}; -Blockly.Bubble.prototype.layoutBubble_=function(){var a=this.workspace_.getMetrics();a.viewLeft/=this.workspace_.scale;a.viewWidth/=this.workspace_.scale;a.viewTop/=this.workspace_.scale;a.viewHeight/=this.workspace_.scale;var b=this.getOptimalRelativeLeft_(a),c=this.getOptimalRelativeTop_(a),d=this.shape_.getBBox(),e={x:b,y:-this.height_-Blockly.BlockSvg.MIN_BLOCK_Y},f={x:-this.width_-30,y:c};c={x:d.width,y:c};var g={x:b,y:d.height};b=d.widtha.viewWidth)return b;if(this.workspace_.RTL)var c=this.anchorXY_.x-b,d=c-this.width_,e=a.viewLeft+a.viewWidth,f=a.viewLeft+Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;else d=b+this.anchorXY_.x,c=d+this.width_,f=a.viewLeft,e=a.viewLeft+a.viewWidth-Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;this.workspace_.RTL?de&&(b=-(e-this.anchorXY_.x)): -de&&(b=e-this.anchorXY_.x-this.width_);return b};Blockly.Bubble.prototype.getOptimalRelativeTop_=function(a){var b=-this.height_/4;if(this.height_>a.viewHeight)return b;var c=this.anchorXY_.y+b,d=c+this.height_,e=a.viewTop;a=a.viewTop+a.viewHeight-Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;var f=this.anchorXY_.y;ca&&(b=a-f-this.height_);return b}; -Blockly.Bubble.prototype.positionBubble_=function(){var a=this.anchorXY_.x;a=this.workspace_.RTL?a-(this.relativeLeft_+this.width_):a+this.relativeLeft_;this.moveTo(a,this.relativeTop_+this.anchorXY_.y)};Blockly.Bubble.prototype.moveTo=function(a,b){this.bubbleGroup_.setAttribute("transform","translate("+a+","+b+")")};Blockly.Bubble.prototype.getBubbleSize=function(){return new Blockly.utils.Size(this.width_,this.height_)}; -Blockly.Bubble.prototype.setBubbleSize=function(a,b){var c=2*Blockly.Bubble.BORDER_WIDTH;a=Math.max(a,c+45);b=Math.max(b,c+20);this.width_=a;this.height_=b;this.bubbleBack_.setAttribute("width",a);this.bubbleBack_.setAttribute("height",b);this.resizeGroup_&&(this.workspace_.RTL?this.resizeGroup_.setAttribute("transform","translate("+2*Blockly.Bubble.BORDER_WIDTH+","+(b-c)+") scale(-1 1)"):this.resizeGroup_.setAttribute("transform","translate("+(a-c)+","+(b-c)+")"));this.autoLayout_&&this.layoutBubble_(); -this.positionBubble_();this.renderArrow_();this.resizeCallback_&&this.resizeCallback_()}; -Blockly.Bubble.prototype.renderArrow_=function(){var a=[],b=this.width_/2,c=this.height_/2,d=-this.relativeLeft_,e=-this.relativeTop_;if(b==d&&c==e)a.push("M "+b+","+c);else{e-=c;d-=b;this.workspace_.RTL&&(d*=-1);var f=Math.sqrt(e*e+d*d),g=Math.acos(d/f);0>e&&(g=2*Math.PI-g);var h=g+Math.PI/2;h>2*Math.PI&&(h-=2*Math.PI);var k=Math.sin(h),l=Math.cos(h),m=this.getBubbleSize();h=(m.width+m.height)/Blockly.Bubble.ARROW_THICKNESS;h=Math.min(h,m.width,m.height)/4;m=1-Blockly.Bubble.ANCHOR_RADIUS/f;d=b+ -m*d;e=c+m*e;m=b+h*l;var n=c+h*k;b-=h*l;c-=h*k;k=g+this.arrow_radians_;k>2*Math.PI&&(k-=2*Math.PI);g=Math.sin(k)*f/Blockly.Bubble.ARROW_BEND;f=Math.cos(k)*f/Blockly.Bubble.ARROW_BEND;a.push("M"+m+","+n);a.push("C"+(m+f)+","+(n+g)+" "+d+","+e+" "+d+","+e);a.push("C"+d+","+e+" "+(b+f)+","+(c+g)+" "+b+","+c)}a.push("z");this.bubbleArrow_.setAttribute("d",a.join(" "))};Blockly.Bubble.prototype.setColour=function(a){this.bubbleBack_.setAttribute("fill",a);this.bubbleArrow_.setAttribute("fill",a)}; -Blockly.Bubble.prototype.dispose=function(){Blockly.Bubble.unbindDragEvents_();Blockly.utils.dom.removeNode(this.bubbleGroup_);this.shape_=this.content_=this.workspace_=this.resizeGroup_=this.bubbleBack_=this.bubbleArrow_=this.bubbleGroup_=null};Blockly.Bubble.prototype.moveDuringDrag=function(a,b){a?a.translateSurface(b.x,b.y):this.moveTo(b.x,b.y);this.relativeLeft_=this.workspace_.RTL?this.anchorXY_.x-b.x-this.width_:b.x-this.anchorXY_.x;this.relativeTop_=b.y-this.anchorXY_.y;this.renderArrow_()}; -Blockly.Bubble.prototype.getRelativeToSurfaceXY=function(){return new Blockly.utils.Coordinate(this.anchorXY_.x+this.relativeLeft_,this.anchorXY_.y+this.relativeTop_)};Blockly.Bubble.prototype.setAutoLayout=function(a){this.autoLayout_=a};Blockly.Events.CommentBase=function(a){this.commentId=a.id;this.workspaceId=a.workspace.id;this.group=Blockly.Events.getGroup();this.recordUndo=Blockly.Events.recordUndo};Blockly.utils.object.inherits(Blockly.Events.CommentBase,Blockly.Events.Abstract);Blockly.Events.CommentBase.prototype.toJson=function(){var a=Blockly.Events.CommentBase.superClass_.toJson.call(this);this.commentId&&(a.commentId=this.commentId);return a}; -Blockly.Events.CommentBase.prototype.fromJson=function(a){Blockly.Events.CommentBase.superClass_.fromJson.call(this,a);this.commentId=a.commentId};Blockly.Events.CommentChange=function(a,b,c){a&&(Blockly.Events.CommentChange.superClass_.constructor.call(this,a),this.oldContents_=b,this.newContents_=c)};Blockly.utils.object.inherits(Blockly.Events.CommentChange,Blockly.Events.CommentBase);Blockly.Events.CommentChange.prototype.type=Blockly.Events.COMMENT_CHANGE; -Blockly.Events.CommentChange.prototype.toJson=function(){var a=Blockly.Events.CommentChange.superClass_.toJson.call(this);a.newContents=this.newContents_;return a};Blockly.Events.CommentChange.prototype.fromJson=function(a){Blockly.Events.CommentChange.superClass_.fromJson.call(this,a);this.newContents_=a.newValue};Blockly.Events.CommentChange.prototype.isNull=function(){return this.oldContents_==this.newContents_}; -Blockly.Events.CommentChange.prototype.run=function(a){var b=this.getEventWorkspace_().getCommentById(this.commentId);b?b.setContent(a?this.newContents_:this.oldContents_):console.warn("Can't change non-existent comment: "+this.commentId)};Blockly.Events.CommentCreate=function(a){a&&(Blockly.Events.CommentCreate.superClass_.constructor.call(this,a),this.xml=a.toXmlWithXY())};Blockly.utils.object.inherits(Blockly.Events.CommentCreate,Blockly.Events.CommentBase); -Blockly.Events.CommentCreate.prototype.type=Blockly.Events.COMMENT_CREATE;Blockly.Events.CommentCreate.prototype.toJson=function(){var a=Blockly.Events.CommentCreate.superClass_.toJson.call(this);a.xml=Blockly.Xml.domToText(this.xml);return a};Blockly.Events.CommentCreate.prototype.fromJson=function(a){Blockly.Events.CommentCreate.superClass_.fromJson.call(this,a);this.xml=Blockly.Xml.textToDom(a.xml)}; -Blockly.Events.CommentCreate.prototype.run=function(a){Blockly.Events.CommentCreateDeleteHelper(this,a)};Blockly.Events.CommentCreateDeleteHelper=function(a,b){var c=a.getEventWorkspace_();if(b){var d=Blockly.utils.xml.createElement("xml");d.appendChild(a.xml);Blockly.Xml.domToWorkspace(d,c)}else(c=c.getCommentById(a.commentId))?c.dispose(!1,!1):console.warn("Can't uncreate non-existent comment: "+a.commentId)}; -Blockly.Events.CommentDelete=function(a){a&&(Blockly.Events.CommentDelete.superClass_.constructor.call(this,a),this.xml=a.toXmlWithXY())};Blockly.utils.object.inherits(Blockly.Events.CommentDelete,Blockly.Events.CommentBase);Blockly.Events.CommentDelete.prototype.type=Blockly.Events.COMMENT_DELETE;Blockly.Events.CommentDelete.prototype.toJson=function(){return Blockly.Events.CommentDelete.superClass_.toJson.call(this)}; -Blockly.Events.CommentDelete.prototype.fromJson=function(a){Blockly.Events.CommentDelete.superClass_.fromJson.call(this,a)};Blockly.Events.CommentDelete.prototype.run=function(a){Blockly.Events.CommentCreateDeleteHelper(this,!a)};Blockly.Events.CommentMove=function(a){a&&(Blockly.Events.CommentMove.superClass_.constructor.call(this,a),this.comment_=a,this.oldCoordinate_=a.getXY(),this.newCoordinate_=null)};Blockly.utils.object.inherits(Blockly.Events.CommentMove,Blockly.Events.CommentBase); -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.dragBubble=function(a,b){var c=this.pixelsToWorkspaceUnits_(b);c=Blockly.utils.Coordinate.sum(this.startXY_,c);this.draggingBubble_.moveDuringDrag(this.dragSurface_,c);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);var c=this.pixelsToWorkspaceUnits_(b);c=Blockly.utils.Coordinate.sum(this.startXY_,c);this.draggingBubble_.moveTo(c.x,c.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_.toolbox_&&(c=this.draggingBubble_.isDeletable()? -"blocklyToolboxDelete":"blocklyToolboxGrab",this.workspace_.toolbox_.removeStyle(c));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.constants={};Blockly.LINE_MODE_MULTIPLIER=40;Blockly.PAGE_MODE_MULTIPLIER=125;Blockly.DRAG_RADIUS=5;Blockly.FLYOUT_DRAG_RADIUS=10;Blockly.SNAP_RADIUS=28;Blockly.CONNECTING_SNAP_RADIUS=Blockly.SNAP_RADIUS;Blockly.CURRENT_CONNECTION_PREFERENCE=8;Blockly.INSERTION_MARKER_COLOUR="#000000";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.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.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.Tooltip={};Blockly.Tooltip.visible=!1;Blockly.Tooltip.blocked_=!1;Blockly.Tooltip.LIMIT=50;Blockly.Tooltip.mouseOutPid_=0;Blockly.Tooltip.showPid_=0;Blockly.Tooltip.lastX_=0;Blockly.Tooltip.lastY_=0;Blockly.Tooltip.element_=null;Blockly.Tooltip.poisonedElement_=null;Blockly.Tooltip.OFFSET_X=0;Blockly.Tooltip.OFFSET_Y=10;Blockly.Tooltip.RADIUS_OK=10;Blockly.Tooltip.HOVER_MS=750;Blockly.Tooltip.MARGINS=5;Blockly.Tooltip.DIV=null; +Blockly.Scrollbar.prototype.setOrigin=function(a,b){this.origin_=new Blockly.utils.Coordinate(a,b)};Blockly.Tooltip={};Blockly.Tooltip.visible=!1;Blockly.Tooltip.blocked_=!1;Blockly.Tooltip.LIMIT=50;Blockly.Tooltip.mouseOutPid_=0;Blockly.Tooltip.showPid_=0;Blockly.Tooltip.lastX_=0;Blockly.Tooltip.lastY_=0;Blockly.Tooltip.element_=null;Blockly.Tooltip.poisonedElement_=null;Blockly.Tooltip.OFFSET_X=0;Blockly.Tooltip.OFFSET_Y=10;Blockly.Tooltip.RADIUS_OK=10;Blockly.Tooltip.HOVER_MS=750;Blockly.Tooltip.MARGINS=5;Blockly.Tooltip.DIV=null; Blockly.Tooltip.createDom=function(){Blockly.Tooltip.DIV||(Blockly.Tooltip.DIV=document.createElement("div"),Blockly.Tooltip.DIV.className="blocklyTooltipDiv",document.body.appendChild(Blockly.Tooltip.DIV))};Blockly.Tooltip.bindMouseEvents=function(a){Blockly.bindEvent_(a,"mouseover",null,Blockly.Tooltip.onMouseOver_);Blockly.bindEvent_(a,"mouseout",null,Blockly.Tooltip.onMouseOut_);a.addEventListener("mousemove",Blockly.Tooltip.onMouseMove_,!1)}; Blockly.Tooltip.onMouseOver_=function(a){if(!Blockly.Tooltip.blocked_){for(a=a.currentTarget;"string"!=typeof a.tooltip&&"function"!=typeof a.tooltip;)a=a.tooltip;Blockly.Tooltip.element_!=a&&(Blockly.Tooltip.hide(),Blockly.Tooltip.poisonedElement_=null,Blockly.Tooltip.element_=a);clearTimeout(Blockly.Tooltip.mouseOutPid_)}}; Blockly.Tooltip.onMouseOut_=function(a){Blockly.Tooltip.blocked_||(Blockly.Tooltip.mouseOutPid_=setTimeout(function(){Blockly.Tooltip.element_=null;Blockly.Tooltip.poisonedElement_=null;Blockly.Tooltip.hide()},1),clearTimeout(Blockly.Tooltip.showPid_))}; @@ -291,77 +211,271 @@ Blockly.Tooltip.onMouseMove_=function(a){if(Blockly.Tooltip.element_&&Blockly.To Blockly.Tooltip.HOVER_MS))};Blockly.Tooltip.hide=function(){Blockly.Tooltip.visible&&(Blockly.Tooltip.visible=!1,Blockly.Tooltip.DIV&&(Blockly.Tooltip.DIV.style.display="none"));Blockly.Tooltip.showPid_&&clearTimeout(Blockly.Tooltip.showPid_)};Blockly.Tooltip.block=function(){Blockly.Tooltip.hide();Blockly.Tooltip.blocked_=!0};Blockly.Tooltip.unblock=function(){Blockly.Tooltip.blocked_=!1}; Blockly.Tooltip.show_=function(){if(!Blockly.Tooltip.blocked_&&(Blockly.Tooltip.poisonedElement_=Blockly.Tooltip.element_,Blockly.Tooltip.DIV)){Blockly.Tooltip.DIV.innerHTML="";for(var a=Blockly.Tooltip.element_.tooltip;"function"==typeof a;)a=a();a=Blockly.utils.string.wrap(a,Blockly.Tooltip.LIMIT);a=a.split("\n");for(var b=0;bc+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.Gesture=function(a,b){this.startWorkspace_=this.targetBlock_=this.startBlock_=this.startField_=this.startBubble_=this.currentDragDeltaXY_=this.mouseDownXY_=null;this.creatorWorkspace_=b;this.isDraggingBubble_=this.isDraggingBlock_=this.isDraggingWorkspace_=this.hasExceededDragRadius_=!1;this.mostRecentEvent_=a;this.flyout_=this.workspaceDragger_=this.blockDragger_=this.bubbleDragger_=this.onUpWrapper_=this.onMoveWrapper_=null;this.isEnding_=this.hasStarted_=this.calledUpdateIsDragging_=!1; -this.healStack_=!Blockly.DRAG_STACK}; -Blockly.Gesture.prototype.dispose=function(){Blockly.Touch.clearTouchIdentifier();Blockly.Tooltip.unblock();this.creatorWorkspace_.clearGesture();this.onMoveWrapper_&&Blockly.unbindEvent_(this.onMoveWrapper_);this.onUpWrapper_&&Blockly.unbindEvent_(this.onUpWrapper_);this.flyout_=this.startWorkspace_=this.targetBlock_=this.startBlock_=this.startField_=null;this.blockDragger_&&(this.blockDragger_.dispose(),this.blockDragger_=null);this.workspaceDragger_&&(this.workspaceDragger_.dispose(),this.workspaceDragger_= -null);this.bubbleDragger_&&(this.bubbleDragger_.dispose(),this.bubbleDragger_=null)};Blockly.Gesture.prototype.updateFromEvent_=function(a){var b=new Blockly.utils.Coordinate(a.clientX,a.clientY);this.updateDragDelta_(b)&&(this.updateIsDragging_(),Blockly.longStop_());this.mostRecentEvent_=a}; +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; +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.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.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); +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; +Blockly.Bubble.prototype.createDom_=function(a,b){this.bubbleGroup_=Blockly.utils.dom.createSvgElement("g",{},null);var c={filter:"url(#"+this.workspace_.getRenderer().getConstants().embossFilterId+")"};Blockly.utils.userAgent.JAVA_FX&&(c={});c=Blockly.utils.dom.createSvgElement("g",c,this.bubbleGroup_);this.bubbleArrow_=Blockly.utils.dom.createSvgElement("path",{},c);this.bubbleBack_=Blockly.utils.dom.createSvgElement("rect",{"class":"blocklyDraggable",x:0,y:0,rx:Blockly.Bubble.BORDER_WIDTH,ry:Blockly.Bubble.BORDER_WIDTH}, +c);b?(this.resizeGroup_=Blockly.utils.dom.createSvgElement("g",{"class":this.workspace_.RTL?"blocklyResizeSW":"blocklyResizeSE"},this.bubbleGroup_),b=2*Blockly.Bubble.BORDER_WIDTH,Blockly.utils.dom.createSvgElement("polygon",{points:"0,x x,x x,0".replace(/x/g,b.toString())},this.resizeGroup_),Blockly.utils.dom.createSvgElement("line",{"class":"blocklyResizeLine",x1:b/3,y1:b-1,x2:b-1,y2:b/3},this.resizeGroup_),Blockly.utils.dom.createSvgElement("line",{"class":"blocklyResizeLine",x1:2*b/3,y1:b-1,x2:b- +1,y2:2*b/3},this.resizeGroup_)):this.resizeGroup_=null;this.workspace_.options.readOnly||(this.onMouseDownBubbleWrapper_=Blockly.bindEventWithChecks_(this.bubbleBack_,"mousedown",this,this.bubbleMouseDown_),this.resizeGroup_&&(this.onMouseDownResizeWrapper_=Blockly.bindEventWithChecks_(this.resizeGroup_,"mousedown",this,this.resizeMouseDown_)));this.bubbleGroup_.appendChild(a);return this.bubbleGroup_};Blockly.Bubble.prototype.getSvgRoot=function(){return this.bubbleGroup_}; +Blockly.Bubble.prototype.setSvgId=function(a){this.bubbleGroup_.dataset&&(this.bubbleGroup_.dataset.blockId=a)};Blockly.Bubble.prototype.bubbleMouseDown_=function(a){var b=this.workspace_.getGesture(a);b&&b.handleBubbleStart(a,this)};Blockly.Bubble.prototype.showContextMenu=function(a){};Blockly.Bubble.prototype.isDeletable=function(){return!1}; +Blockly.Bubble.prototype.resizeMouseDown_=function(a){this.promote();Blockly.Bubble.unbindDragEvents_();Blockly.utils.isRightButton(a)||(this.workspace_.startDrag(a,new Blockly.utils.Coordinate(this.workspace_.RTL?-this.width_:this.width_,this.height_)),Blockly.Bubble.onMouseUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",this,Blockly.Bubble.bubbleMouseUp_),Blockly.Bubble.onMouseMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",this,this.resizeMouseMove_),Blockly.hideChaff()); +a.stopPropagation()};Blockly.Bubble.prototype.resizeMouseMove_=function(a){this.autoLayout_=!1;a=this.workspace_.moveDrag(a);this.setBubbleSize(this.workspace_.RTL?-a.x:a.x,a.y);this.workspace_.RTL&&this.positionBubble_()};Blockly.Bubble.prototype.registerResizeEvent=function(a){this.resizeCallback_=a};Blockly.Bubble.prototype.registerMoveEvent=function(a){this.moveCallback_=a}; +Blockly.Bubble.prototype.promote=function(){var a=this.bubbleGroup_.parentNode;return a.lastChild!==this.bubbleGroup_?(a.appendChild(this.bubbleGroup_),!0):!1};Blockly.Bubble.prototype.setAnchorLocation=function(a){this.anchorXY_=a;this.rendered_&&this.positionBubble_()}; +Blockly.Bubble.prototype.layoutBubble_=function(){var a=this.workspace_.getMetrics();a.viewLeft/=this.workspace_.scale;a.viewWidth/=this.workspace_.scale;a.viewTop/=this.workspace_.scale;a.viewHeight/=this.workspace_.scale;var b=this.getOptimalRelativeLeft_(a),c=this.getOptimalRelativeTop_(a),d=this.shape_.getBBox(),e={x:b,y:-this.height_-this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT},f={x:-this.width_-30,y:c};c={x:d.width,y:c};var g={x:b,y:d.height};b=d.widtha.viewWidth)return b;if(this.workspace_.RTL)var c=this.anchorXY_.x-b,d=c-this.width_,e=a.viewLeft+a.viewWidth,f=a.viewLeft+Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;else d=b+this.anchorXY_.x,c=d+this.width_,f=a.viewLeft,e=a.viewLeft+a.viewWidth-Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;this.workspace_.RTL?de&&(b=-(e-this.anchorXY_.x)): +de&&(b=e-this.anchorXY_.x-this.width_);return b};Blockly.Bubble.prototype.getOptimalRelativeTop_=function(a){var b=-this.height_/4;if(this.height_>a.viewHeight)return b;var c=this.anchorXY_.y+b,d=c+this.height_,e=a.viewTop;a=a.viewTop+a.viewHeight-Blockly.Scrollbar.scrollbarThickness/this.workspace_.scale;var f=this.anchorXY_.y;ca&&(b=a-f-this.height_);return b}; +Blockly.Bubble.prototype.positionBubble_=function(){var a=this.anchorXY_.x;a=this.workspace_.RTL?a-(this.relativeLeft_+this.width_):a+this.relativeLeft_;this.moveTo(a,this.relativeTop_+this.anchorXY_.y)};Blockly.Bubble.prototype.moveTo=function(a,b){this.bubbleGroup_.setAttribute("transform","translate("+a+","+b+")")};Blockly.Bubble.prototype.setDragging=function(a){!a&&this.moveCallback_&&this.moveCallback_()}; +Blockly.Bubble.prototype.getBubbleSize=function(){return new Blockly.utils.Size(this.width_,this.height_)}; +Blockly.Bubble.prototype.setBubbleSize=function(a,b){var c=2*Blockly.Bubble.BORDER_WIDTH;a=Math.max(a,c+45);b=Math.max(b,c+20);this.width_=a;this.height_=b;this.bubbleBack_.setAttribute("width",a);this.bubbleBack_.setAttribute("height",b);this.resizeGroup_&&(this.workspace_.RTL?this.resizeGroup_.setAttribute("transform","translate("+2*Blockly.Bubble.BORDER_WIDTH+","+(b-c)+") scale(-1 1)"):this.resizeGroup_.setAttribute("transform","translate("+(a-c)+","+(b-c)+")"));this.autoLayout_&&this.layoutBubble_(); +this.positionBubble_();this.renderArrow_();this.resizeCallback_&&this.resizeCallback_()}; +Blockly.Bubble.prototype.renderArrow_=function(){var a=[],b=this.width_/2,c=this.height_/2,d=-this.relativeLeft_,e=-this.relativeTop_;if(b==d&&c==e)a.push("M "+b+","+c);else{e-=c;d-=b;this.workspace_.RTL&&(d*=-1);var f=Math.sqrt(e*e+d*d),g=Math.acos(d/f);0>e&&(g=2*Math.PI-g);var h=g+Math.PI/2;h>2*Math.PI&&(h-=2*Math.PI);var k=Math.sin(h),l=Math.cos(h),m=this.getBubbleSize();h=(m.width+m.height)/Blockly.Bubble.ARROW_THICKNESS;h=Math.min(h,m.width,m.height)/4;m=1-Blockly.Bubble.ANCHOR_RADIUS/f;d=b+ +m*d;e=c+m*e;m=b+h*l;var n=c+h*k;b-=h*l;c-=h*k;k=g+this.arrow_radians_;k>2*Math.PI&&(k-=2*Math.PI);g=Math.sin(k)*f/Blockly.Bubble.ARROW_BEND;f=Math.cos(k)*f/Blockly.Bubble.ARROW_BEND;a.push("M"+m+","+n);a.push("C"+(m+f)+","+(n+g)+" "+d+","+e+" "+d+","+e);a.push("C"+d+","+e+" "+(b+f)+","+(c+g)+" "+b+","+c)}a.push("z");this.bubbleArrow_.setAttribute("d",a.join(" "))};Blockly.Bubble.prototype.setColour=function(a){this.bubbleBack_.setAttribute("fill",a);this.bubbleArrow_.setAttribute("fill",a)}; +Blockly.Bubble.prototype.dispose=function(){this.onMouseDownBubbleWrapper_&&Blockly.unbindEvent_(this.onMouseDownBubbleWrapper_);this.onMouseDownResizeWrapper_&&Blockly.unbindEvent_(this.onMouseDownResizeWrapper_);Blockly.Bubble.unbindDragEvents_();Blockly.utils.dom.removeNode(this.bubbleGroup_);this.disposed=!0}; +Blockly.Bubble.prototype.moveDuringDrag=function(a,b){a?a.translateSurface(b.x,b.y):this.moveTo(b.x,b.y);this.relativeLeft_=this.workspace_.RTL?this.anchorXY_.x-b.x-this.width_:b.x-this.anchorXY_.x;this.relativeTop_=b.y-this.anchorXY_.y;this.renderArrow_()};Blockly.Bubble.prototype.getRelativeToSurfaceXY=function(){return new Blockly.utils.Coordinate(this.workspace_.RTL?-this.relativeLeft_+this.anchorXY_.x-this.width_:this.anchorXY_.x+this.relativeLeft_,this.anchorXY_.y+this.relativeTop_)}; +Blockly.Bubble.prototype.setAutoLayout=function(a){this.autoLayout_=a};Blockly.Events.CommentBase=function(a){this.commentId=a.id;this.workspaceId=a.workspace.id;this.group=Blockly.Events.getGroup();this.recordUndo=Blockly.Events.recordUndo};Blockly.utils.object.inherits(Blockly.Events.CommentBase,Blockly.Events.Abstract);Blockly.Events.CommentBase.prototype.toJson=function(){var a=Blockly.Events.CommentBase.superClass_.toJson.call(this);this.commentId&&(a.commentId=this.commentId);return a}; +Blockly.Events.CommentBase.prototype.fromJson=function(a){Blockly.Events.CommentBase.superClass_.fromJson.call(this,a);this.commentId=a.commentId};Blockly.Events.CommentChange=function(a,b,c){a&&(Blockly.Events.CommentChange.superClass_.constructor.call(this,a),this.oldContents_=b,this.newContents_=c)};Blockly.utils.object.inherits(Blockly.Events.CommentChange,Blockly.Events.CommentBase);Blockly.Events.CommentChange.prototype.type=Blockly.Events.COMMENT_CHANGE; +Blockly.Events.CommentChange.prototype.toJson=function(){var a=Blockly.Events.CommentChange.superClass_.toJson.call(this);a.newContents=this.newContents_;return a};Blockly.Events.CommentChange.prototype.fromJson=function(a){Blockly.Events.CommentChange.superClass_.fromJson.call(this,a);this.newContents_=a.newValue};Blockly.Events.CommentChange.prototype.isNull=function(){return this.oldContents_==this.newContents_}; +Blockly.Events.CommentChange.prototype.run=function(a){var b=this.getEventWorkspace_().getCommentById(this.commentId);b?b.setContent(a?this.newContents_:this.oldContents_):console.warn("Can't change non-existent comment: "+this.commentId)};Blockly.Events.CommentCreate=function(a){a&&(Blockly.Events.CommentCreate.superClass_.constructor.call(this,a),this.xml=a.toXmlWithXY())};Blockly.utils.object.inherits(Blockly.Events.CommentCreate,Blockly.Events.CommentBase); +Blockly.Events.CommentCreate.prototype.type=Blockly.Events.COMMENT_CREATE;Blockly.Events.CommentCreate.prototype.toJson=function(){var a=Blockly.Events.CommentCreate.superClass_.toJson.call(this);a.xml=Blockly.Xml.domToText(this.xml);return a};Blockly.Events.CommentCreate.prototype.fromJson=function(a){Blockly.Events.CommentCreate.superClass_.fromJson.call(this,a);this.xml=Blockly.Xml.textToDom(a.xml)}; +Blockly.Events.CommentCreate.prototype.run=function(a){Blockly.Events.CommentCreateDeleteHelper(this,a)};Blockly.Events.CommentCreateDeleteHelper=function(a,b){var c=a.getEventWorkspace_();b?(b=Blockly.utils.xml.createElement("xml"),b.appendChild(a.xml),Blockly.Xml.domToWorkspace(b,c)):(c=c.getCommentById(a.commentId))?c.dispose(!1,!1):console.warn("Can't uncreate non-existent comment: "+a.commentId)}; +Blockly.Events.CommentDelete=function(a){a&&(Blockly.Events.CommentDelete.superClass_.constructor.call(this,a),this.xml=a.toXmlWithXY())};Blockly.utils.object.inherits(Blockly.Events.CommentDelete,Blockly.Events.CommentBase);Blockly.Events.CommentDelete.prototype.type=Blockly.Events.COMMENT_DELETE;Blockly.Events.CommentDelete.prototype.toJson=function(){return Blockly.Events.CommentDelete.superClass_.toJson.call(this)}; +Blockly.Events.CommentDelete.prototype.fromJson=function(a){Blockly.Events.CommentDelete.superClass_.fromJson.call(this,a)};Blockly.Events.CommentDelete.prototype.run=function(a){Blockly.Events.CommentCreateDeleteHelper(this,!a)};Blockly.Events.CommentMove=function(a){a&&(Blockly.Events.CommentMove.superClass_.constructor.call(this,a),this.comment_=a,this.oldCoordinate_=a.getXY(),this.newCoordinate_=null)};Blockly.utils.object.inherits(Blockly.Events.CommentMove,Blockly.Events.CommentBase); +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.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.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.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.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(),this.startWorkspace_.markFocused(),this.mostRecentEvent_=a,Blockly.hideChaff(!!this.flyout_),Blockly.Tooltip.block(),this.targetBlock_&&(!this.targetBlock_.isInFlyout&&a.shiftKey?(Blockly.navigation.enableKeyboardAccessibility(), -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)))}; +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)))}; 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);Blockly.keyboardAccessibilityMode&&Blockly.navigation.setState(Blockly.navigation.STATE_WS)}; +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.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.bringBlockToFront_()}; +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?(Blockly.navigation.enableKeyboardAccessibility(),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()};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.markerSvg_=this.cursorSvg_=null;c&&this.configure_(c);this.setValue(a);b&&this.setValidator(b)};Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT=16;Blockly.Field.TEXT_DEFAULT_HEIGHT=12.5;Blockly.Field.X_PADDING=10;Blockly.Field.Y_PADDING=10;Blockly.Field.DEFAULT_TEXT_OFFSET=Blockly.Field.X_PADDING/2;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.getSourceBlock=function(){return this.sourceBlock_}; +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.Field.prototype.setSourceBlock=function(a){if(this.sourceBlock_)throw Error("Field already bound to a block.");this.sourceBlock_=a;a.workspace.rendered&&(this.constants_=a.workspace.getRenderer().getConstants())};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.size_.height=Math.max(this.size_.height,Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT);this.size_.width=Math.max(this.size_.width,Blockly.Field.X_PADDING);this.borderRect_=Blockly.utils.dom.createSvgElement("rect",{rx:4,ry:4,x:0,y:0,height:this.size_.height,width:this.size_.width},this.fieldGroup_)}; -Blockly.Field.prototype.createTextElement_=function(){this.textElement_=Blockly.utils.dom.createSvgElement("text",{"class":"blocklyText",y:Blockly.Field.TEXT_DEFAULT_HEIGHT,x:this.borderRect_?Blockly.Field.DEFAULT_TEXT_OFFSET:0},this.fieldGroup_);this.textContent_=document.createTextNode("");this.textElement_.appendChild(this.textContent_)}; -Blockly.Field.prototype.bindEvents_=function(){Blockly.Tooltip.bindMouseEvents(this.getClickTarget_());this.mouseDownWrapper_=Blockly.bindEventWithChecks_(this.getClickTarget_(),"mousedown",this,this.onMouseDown_)};Blockly.Field.prototype.fromXml=function(a){this.setValue(a.textContent)};Blockly.Field.prototype.toXml=function(a){a.textContent=this.getValue();return a}; -Blockly.Field.prototype.dispose=function(){Blockly.DropDownDiv.hideIfOwner(this);Blockly.WidgetDiv.hideIfOwner(this);this.mouseDownWrapper_&&Blockly.unbindEvent_(this.mouseDownWrapper_);Blockly.utils.dom.removeNode(this.fieldGroup_);this.disposed=!0}; -Blockly.Field.prototype.updateEditable=function(){var a=this.getClickTarget_();this.EDITABLE&&a&&(this.sourceBlock_.isEditable()?(Blockly.utils.dom.addClass(a,"blocklyEditableText"),Blockly.utils.dom.removeClass(a,"blocklyNonEditableText"),a.style.cursor=this.CURSOR):(Blockly.utils.dom.addClass(a,"blocklyNonEditableText"),Blockly.utils.dom.removeClass(a,"blocklyEditableText"),a.style.cursor=""))}; +Blockly.Field.prototype.createBorderRect_=function(){this.size_.height=Math.max(this.size_.height,this.constants_.FIELD_BORDER_RECT_HEIGHT);this.size_.width=Math.max(this.size_.width,2*this.constants_.FIELD_BORDER_RECT_X_PADDING);this.borderRect_=Blockly.utils.dom.createSvgElement("rect",{rx:this.constants_.FIELD_BORDER_RECT_RADIUS,ry:this.constants_.FIELD_BORDER_RECT_RADIUS,x:0,y:0,height:this.size_.height,width:this.size_.width,"class":"blocklyFieldRect"},this.fieldGroup_)}; +Blockly.Field.prototype.createTextElement_=function(){var a=this.borderRect_?this.constants_.FIELD_BORDER_RECT_X_PADDING:0,b=this.constants_.FIELD_TEXT_BASELINE_CENTER,c=this.constants_.FIELD_TEXT_BASELINE_Y;this.size_.height=Math.max(this.size_.height,b?this.constants_.FIELD_TEXT_HEIGHT:c);this.size_.height>this.constants_.FIELD_TEXT_HEIGHT&&(c+=(this.size_.height-c)/2);this.textElement_=Blockly.utils.dom.createSvgElement("text",{"class":"blocklyText",y:b?this.size_.height/2:c,dy:this.constants_.FIELD_TEXT_Y_OFFSET, +x:a},this.fieldGroup_);b&&this.textElement_.setAttribute("dominant-baseline","central");this.textContent_=document.createTextNode("");this.textElement_.appendChild(this.textContent_)};Blockly.Field.prototype.bindEvents_=function(){Blockly.Tooltip.bindMouseEvents(this.getClickTarget_());this.mouseDownWrapper_=Blockly.bindEventWithChecks_(this.getClickTarget_(),"mousedown",this,this.onMouseDown_)};Blockly.Field.prototype.fromXml=function(a){this.setValue(a.textContent)}; +Blockly.Field.prototype.toXml=function(a){a.textContent=this.getValue();return a};Blockly.Field.prototype.dispose=function(){Blockly.DropDownDiv.hideIfOwner(this);Blockly.WidgetDiv.hideIfOwner(this);this.mouseDownWrapper_&&Blockly.unbindEvent_(this.mouseDownWrapper_);Blockly.utils.dom.removeNode(this.fieldGroup_);this.disposed=!0}; +Blockly.Field.prototype.updateEditable=function(){var a=this.fieldGroup_;this.EDITABLE&&a&&(this.sourceBlock_.isEditable()?(Blockly.utils.dom.addClass(a,"blocklyEditableText"),Blockly.utils.dom.removeClass(a,"blocklyNonEditableText"),a.style.cursor=this.CURSOR):(Blockly.utils.dom.addClass(a,"blocklyNonEditableText"),Blockly.utils.dom.removeClass(a,"blocklyEditableText"),a.style.cursor=""))}; Blockly.Field.prototype.isClickable=function(){return!!this.sourceBlock_&&this.sourceBlock_.isEditable()&&!!this.showEditor_&&"function"===typeof this.showEditor_};Blockly.Field.prototype.isCurrentlyEditable=function(){return this.EDITABLE&&!!this.sourceBlock_&&this.sourceBlock_.isEditable()}; Blockly.Field.prototype.isSerializable=function(){var a=!1;this.name&&(this.SERIALIZABLE?a=!0:this.EDITABLE&&(console.warn("Detected an editable field that was not serializable. Please define SERIALIZABLE property as true on all editable custom fields. Proceeding with serialization."),a=!0));return a};Blockly.Field.prototype.isVisible=function(){return this.visible_}; Blockly.Field.prototype.setVisible=function(a){if(this.visible_!=a){this.visible_=a;var b=this.getSvgRoot();b&&(b.style.display=a?"block":"none")}};Blockly.Field.prototype.setValidator=function(a){this.validator_=a};Blockly.Field.prototype.getValidator=function(){return this.validator_};Blockly.Field.prototype.classValidator=function(a){return a}; -Blockly.Field.prototype.callValidator=function(a){var b=this.classValidator(a);if(null===b)return null;void 0!==b&&(a=b);if(b=this.getValidator()){b=b.call(this,a);if(null===b)return null;void 0!==b&&(a=b)}return a};Blockly.Field.prototype.getSvgRoot=function(){return this.fieldGroup_};Blockly.Field.prototype.updateColour=function(){};Blockly.Field.prototype.render_=function(){this.textContent_&&(this.textContent_.nodeValue=this.getDisplayText_(),this.updateSize_())}; -Blockly.Field.prototype.updateWidth=function(){console.warn("Deprecated call to updateWidth, call Blockly.Field.updateSize_ to force an update to the size of the field, or Blockly.utils.dom.getTextWidth() to check the size of the field.");this.updateSize_()};Blockly.Field.prototype.updateSize_=function(){var a=Blockly.utils.dom.getTextWidth(this.textElement_);this.borderRect_&&(a+=Blockly.Field.X_PADDING,this.borderRect_.setAttribute("width",a));this.size_.width=a}; +Blockly.Field.prototype.callValidator=function(a){var b=this.classValidator(a);if(null===b)return null;void 0!==b&&(a=b);if(b=this.getValidator()){b=b.call(this,a);if(null===b)return null;void 0!==b&&(a=b)}return a};Blockly.Field.prototype.getSvgRoot=function(){return this.fieldGroup_};Blockly.Field.prototype.applyColour=function(){};Blockly.Field.prototype.render_=function(){this.textContent_&&(this.textContent_.nodeValue=this.getDisplayText_(),this.updateSize_())}; +Blockly.Field.prototype.showEditor=function(a){this.isClickable()&&this.showEditor_(a)};Blockly.Field.prototype.updateWidth=function(){console.warn("Deprecated call to updateWidth, call Blockly.Field.updateSize_ to force an update to the size of the field, or Blockly.utils.dom.getTextWidth() to check the size of the field.");this.updateSize_()}; +Blockly.Field.prototype.updateSize_=function(){var a=Blockly.utils.dom.getFastTextWidth(this.textElement_,this.constants_.FIELD_TEXT_FONTSIZE,this.constants_.FIELD_TEXT_FONTWEIGHT,this.constants_.FIELD_TEXT_FONTFAMILY);this.borderRect_&&(a+=2*this.constants_.FIELD_BORDER_RECT_X_PADDING,this.borderRect_.setAttribute("width",a));this.size_.width=a}; 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(){var a=this.borderRect_.getBBox(),b=a.height*this.sourceBlock_.workspace.scale;a=a.width*this.sourceBlock_.workspace.scale;var c=this.getAbsoluteXY_();return{top:c.y,bottom:c.y+b,left:c.x,right:c.x+a}}; -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{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.setText=function(a){throw Error("setText method is deprecated");};Blockly.Field.prototype.markDirty=function(){this.isDirty_=!0};Blockly.Field.prototype.forceRerender=function(){this.isDirty_=!0;this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours())}; 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.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.borderRect_)}; +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.unhideAll():this.connection.hideAll(),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.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&&0<=b&&256>b&&0<=c&&256>c)?Blockly.utils.colour.rgbToHex(a,b,c):null}; -Blockly.utils.colour.rgbToHex=function(a,b,c){b=a<<16|b<<8|c;return 16>a?"#"+(16777216|b).toString(16).substr(1):"#"+b.toString(16)};Blockly.utils.colour.hexToRgb=function(a){a=parseInt(a.substr(1),16);return[a>>16,a>>8&255,a&255]}; -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.hexToRgb(Blockly.utils.colour.parse(a));b=Blockly.utils.colour.hexToRgb(Blockly.utils.colour.parse(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.Block=function(a,b,c){if(Blockly.Generator&&"undefined"!=typeof Blockly.Generator.prototype[b])throw Error('Block prototypeName "'+b+'" conflicts with Blockly.Generator members.');this.id=c&&!a.getBlockById(c)?c:Blockly.utils.genUid();a.blockDB_[this.id]=this;this.previousConnection=this.nextConnection=this.outputConnection=null;this.inputList=[];this.inputsInline=void 0;this.disabled=!1;this.tooltip="";this.contextMenu=!0;this.parentBlock_=null;this.childBlocks_=[];this.editable_=this.movable_= -this.deletable_=!0;this.collapsed_=this.isShadow_=!1;this.comment=null;this.commentModel={text:null,pinned:!1,size:new Blockly.utils.Size(160,80)};this.xy_=new Blockly.utils.Coordinate(0,0);this.workspace=a;this.isInFlyout=a.isFlyout;this.isInMutator=a.isMutator;this.RTL=a.RTL;this.isInsertionMarker_=!1;this.hat=void 0;if(b){this.type=b;c=Blockly.Blocks[b];if(!c||"object"!=typeof c)throw TypeError("Unknown block type: "+b);Blockly.utils.object.mixin(this,c)}a.addTopBlock(this);a.addTypedBlock(this); +Blockly.Input.prototype.dispose=function(){for(var a=0,b;b=this.fieldRow[a];a++)b.dispose();this.connection&&this.connection.dispose();this.sourceBlock_=null};Blockly.Block=function(a,b,c){if(Blockly.Generator&&"undefined"!=typeof Blockly.Generator.prototype[b])throw Error('Block prototypeName "'+b+'" conflicts with Blockly.Generator members.');this.id=c&&!a.getBlockById(c)?c:Blockly.utils.genUid();a.setBlockById(this.id,this);this.previousConnection=this.nextConnection=this.outputConnection=null;this.inputList=[];this.inputsInline=void 0;this.disabled=!1;this.tooltip="";this.contextMenu=!0;this.parentBlock_=null;this.childBlocks_=[];this.editable_=this.movable_= +this.deletable_=!0;this.collapsed_=this.isShadow_=!1;this.comment=this.outputShape_=null;this.commentModel={text:null,pinned:!1,size:new Blockly.utils.Size(160,80)};this.xy_=new Blockly.utils.Coordinate(0,0);this.workspace=a;this.isInFlyout=a.isFlyout;this.isInMutator=a.isMutator;this.RTL=a.RTL;this.isInsertionMarker_=!1;this.hat=void 0;if(b){this.type=b;c=Blockly.Blocks[b];if(!c||"object"!=typeof c)throw TypeError("Unknown block type: "+b);Blockly.utils.object.mixin(this,c)}a.addTopBlock(this);a.addTypedBlock(this); "function"==typeof this.init&&this.init();this.inputsInlineDefault=this.inputsInline;if(Blockly.Events.isEnabled()){(a=Blockly.Events.getGroup())||Blockly.Events.setGroup(!0);try{Blockly.Events.fire(new Blockly.Events.BlockCreate(this))}finally{a||Blockly.Events.setGroup(!1)}}"function"==typeof this.onchange&&this.setOnChange(this.onchange)};Blockly.Block.prototype.data=null;Blockly.Block.prototype.disposed=!1;Blockly.Block.prototype.hue_=null;Blockly.Block.prototype.colour_="#000000"; -Blockly.Block.prototype.colourSecondary_=null;Blockly.Block.prototype.colourTertiary_=null;Blockly.Block.prototype.styleName_=null; -Blockly.Block.prototype.dispose=function(a){if(this.workspace){this.onchangeWrapper_&&this.workspace.removeChangeListener(this.onchangeWrapper_);Blockly.keyboardAccessibilityMode&&Blockly.navigation.moveCursorOnBlockDelete(this);this.unplug(a);Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockDelete(this));Blockly.Events.disable();try{this.workspace&&(this.workspace.removeTopBlock(this),this.workspace.removeTypedBlock(this),delete this.workspace.blockDB_[this.id],this.workspace= -null);Blockly.selected==this&&(Blockly.selected=null);for(var b=this.childBlocks_.length-1;0<=b;b--)this.childBlocks_[b].dispose(!1);b=0;for(var c;c=this.inputList[b];b++)c.dispose();this.inputList.length=0;var d=this.getConnections_(!0);b=0;for(var e;e=d[b];b++)e.dispose()}finally{Blockly.Events.enable(),this.disposed=!0}}};Blockly.Block.prototype.initModel=function(){for(var a=0,b;b=this.inputList[a];a++)for(var c=0,d;d=b.fieldRow[c];c++)d.initModel&&d.initModel()}; -Blockly.Block.prototype.unplug=function(a){this.outputConnection?this.unplugFromRow_(a):this.previousConnection&&this.unplugFromStack_(a)};Blockly.Block.prototype.unplugFromRow_=function(a){var b=null;this.outputConnection.isConnected()&&(b=this.outputConnection.targetConnection,this.outputConnection.disconnect());if(b&&a&&(a=this.getOnlyValueConnection_())&&a.isConnected()&&!a.targetBlock().isShadow())if(a=a.targetConnection,a.disconnect(),a.checkType_(b))b.connect(a);else a.onFailedConnect(b)}; +Blockly.Block.prototype.styleName_=null; +Blockly.Block.prototype.dispose=function(a){if(this.workspace){this.onchangeWrapper_&&this.workspace.removeChangeListener(this.onchangeWrapper_);this.unplug(a);Blockly.Events.isEnabled()&&Blockly.Events.fire(new Blockly.Events.BlockDelete(this));Blockly.Events.disable();try{this.workspace&&(this.workspace.removeTopBlock(this),this.workspace.removeTypedBlock(this),this.workspace.removeBlockById(this.id),this.workspace=null);Blockly.selected==this&&(Blockly.selected=null);for(var b=this.childBlocks_.length- +1;0<=b;b--)this.childBlocks_[b].dispose(!1);b=0;for(var c;c=this.inputList[b];b++)c.dispose();this.inputList.length=0;var d=this.getConnections_(!0);b=0;for(var e;e=d[b];b++)e.dispose()}finally{Blockly.Events.enable(),this.disposed=!0}}};Blockly.Block.prototype.initModel=function(){for(var a=0,b;b=this.inputList[a];a++)for(var c=0,d;d=b.fieldRow[c];c++)d.initModel&&d.initModel()};Blockly.Block.prototype.unplug=function(a){this.outputConnection?this.unplugFromRow_(a):this.previousConnection&&this.unplugFromStack_(a)}; +Blockly.Block.prototype.unplugFromRow_=function(a){var b=null;this.outputConnection.isConnected()&&(b=this.outputConnection.targetConnection,this.outputConnection.disconnect());if(b&&a&&(a=this.getOnlyValueConnection_())&&a.isConnected()&&!a.targetBlock().isShadow())if(a=a.targetConnection,a.disconnect(),a.checkType(b))b.connect(a);else a.onFailedConnect(b)}; Blockly.Block.prototype.getOnlyValueConnection_=function(){for(var a=null,b=0;b=c)this.hue_=c,this.colour_=Blockly.hueToHex(c);else if(c=Blockly.utils.colour.parse(b))this.colour_=c,this.hue_=null;else throw c='Invalid colour: "'+b+'"',a!=b&&(c+=' (from "'+a+'")'),Error(c);}; -Blockly.Block.prototype.setStyle=function(a){var b=this.workspace.getTheme().getBlockStyle(a);this.styleName_=a;if(b)this.colourSecondary_=b.colourSecondary,this.colourTertiary_=b.colourTertiary,this.hat=b.hat,this.setColour(b.colourPrimary);else throw Error("Invalid style name: "+a);}; -Blockly.Block.prototype.setOnChange=function(a){if(a&&"function"!=typeof a)throw Error("onchange must be a function.");this.onchangeWrapper_&&this.workspace.removeChangeListener(this.onchangeWrapper_);if(this.onchange=a)this.onchangeWrapper_=a.bind(this),this.workspace.addChangeListener(this.onchangeWrapper_)};Blockly.Block.prototype.getField=function(a){for(var b=0,c;c=this.inputList[b];b++)for(var d=0,e;e=c.fieldRow[d];d++)if(e.name==a)return e;return null}; -Blockly.Block.prototype.getVars=function(){for(var a=[],b=0,c;c=this.inputList[b];b++)for(var d=0,e;e=c.fieldRow[d];d++)e.referencesVariables()&&a.push(e.getValue());return a};Blockly.Block.prototype.getVarModels=function(){for(var a=[],b=0,c;c=this.inputList[b];b++)for(var d=0,e;e=c.fieldRow[d];d++)e.referencesVariables()&&(e=this.workspace.getVariableById(e.getValue()))&&a.push(e);return a}; -Blockly.Block.prototype.updateVarName=function(a){for(var b=0,c;c=this.inputList[b];b++)for(var d=0,e;e=c.fieldRow[d];d++)e.referencesVariables()&&a.getId()==e.getValue()&&e.refreshVariableName()};Blockly.Block.prototype.renameVarById=function(a,b){for(var c=0,d;d=this.inputList[c];c++)for(var e=0,f;f=d.fieldRow[e];e++)f.referencesVariables()&&a==f.getValue()&&f.setValue(b)};Blockly.Block.prototype.getFieldValue=function(a){return(a=this.getField(a))?a.getValue():null}; -Blockly.Block.prototype.setFieldValue=function(a,b){var c=this.getField(b);if(!c)throw Error('Field "'+b+'" not found.');c.setValue(a)}; +Blockly.Block.prototype.setInsertionMarker=function(a){this.isInsertionMarker_=a};Blockly.Block.prototype.isEditable=function(){return this.editable_&&!(this.workspace&&this.workspace.options.readOnly)};Blockly.Block.prototype.setEditable=function(a){this.editable_=a;a=0;for(var b;b=this.inputList[a];a++)for(var c=0,d;d=b.fieldRow[c];c++)d.updateEditable()};Blockly.Block.prototype.isDisposed=function(){return this.disposed}; +Blockly.Block.prototype.getMatchingConnection=function(a,b){var c=this.getConnections_(!0);a=a.getConnections_(!0);if(c.length!=a.length)throw Error("Connection lists did not match in length.");for(var d=0;da&&(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.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]),c++;void 0!== -a.inputsInline&&this.setInputsInline(a.inputsInline);void 0!==a.output&&this.setOutput(!0,a.output);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=h||h>b.length)throw Error('Block "'+this.type+'": Message index %'+h+" out of range.");if(e[h])throw Error('Block "'+this.type+'": Message index %'+h+" duplicated.");e[h]=!0;f++;a.push(b[h-1])}else(h=h.trim())&&a.push(h)}if(f!=b.length)throw Error('Block "'+this.type+'": Message does not reference all '+b.length+" arg(s)."); -a.length&&("string"==typeof a[a.length-1]||Blockly.utils.string.startsWith(a[a.length-1].type,"field_"))&&(g={type:"input_dummy"},c&&(g.align=c),a.push(g));c={LEFT:Blockly.ALIGN_LEFT,RIGHT:Blockly.ALIGN_RIGHT,CENTRE:Blockly.ALIGN_CENTRE};b=[];for(g=0;g=k||k>b.length)throw Error('Block "'+this.type+'": Message index %'+k+" out of range.");if(f[k])throw Error('Block "'+this.type+'": Message index %'+k+" duplicated.");f[k]=!0;g++;a.push(b[k-1])}else(k=k.trim())&&a.push(k)}if(g!=b.length)throw Error('Block "'+this.type+'": Message does not reference all '+b.length+" arg(s)."); +a.length&&("string"==typeof a[a.length-1]||Blockly.utils.string.startsWith(a[a.length-1].type,"field_"))&&(h={type:"input_dummy"},c&&(h.align=c),a.push(h));c={LEFT:Blockly.ALIGN_LEFT,RIGHT:Blockly.ALIGN_RIGHT,CENTRE:Blockly.ALIGN_CENTRE,CENTER:Blockly.ALIGN_CENTRE};b=[];for(h=0;h=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);ab||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);-1a?b-1:a},this.highlightedIndex_)}; -Blockly.Menu.prototype.highlightHelper=function(a,b){var c=0>b?-1:b,d=this.getChildCount();c=a.call(this,c,d);for(var e=0;e<=d;){var f=this.getChildAt(c);if(f&&this.canHighlightItem(f))return this.setHighlightedIndex(c),!0;e++;c=a.call(this,c,d)}return!1};Blockly.Menu.prototype.canHighlightItem=function(a){return a.isEnabled()};Blockly.Menu.prototype.handleMouseOver_=function(a){(a=this.getMenuItem(a.target))&&a.isEnabled()&&this.getHighlighted()!==a&&(this.unhighlightCurrent(),this.setHighlighted(a))}; -Blockly.Menu.prototype.handleClick_=function(a){var b=this.getMenuItem(a.target);b&&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}; +"keydown",this,this.handleKeyEvent)}; +Blockly.Menu.prototype.detachEvents_=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.onKeyDownWrapper_&&(Blockly.unbindEvent_(this.onKeyDownWrapper_),this.onKeyDownWrapper_= +null)};Blockly.Menu.prototype.childElementIdMap_=null;Blockly.Menu.prototype.registerChildId_=function(a){var b=a.getElement();b=b.id||(b.id=a.getId());this.childElementIdMap_||(this.childElementIdMap_={});this.childElementIdMap_[b]=a};Blockly.Menu.prototype.getMenuItem=function(a){if(this.childElementIdMap_)for(var b=this.getElement();a&&a!==b;){var c=a.id;if(c in this.childElementIdMap_)return this.childElementIdMap_[c];a=a.parentNode}return null}; +Blockly.Menu.prototype.unhighlightCurrent=function(){var a=this.getHighlighted();a&&a.setHighlighted(!1)};Blockly.Menu.prototype.clearHighlighted=function(){this.unhighlightCurrent();this.setHighlightedIndex(-1)};Blockly.Menu.prototype.getHighlighted=function(){return this.getChildAt(this.highlightedIndex_)}; +Blockly.Menu.prototype.setHighlightedIndex=function(a){var b=this.getChildAt(a);b?(b.setHighlighted(!0),this.highlightedIndex_=a):-1a?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){(a=this.getMenuItem(a.target))&&a.isEnabled()&&this.getHighlighted()!==a&&(this.unhighlightCurrent(),this.setHighlighted(a))}; +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.isRightToLeft()?"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}; @@ -442,342 +538,233 @@ Blockly.MenuItem.prototype.setHighlighted=function(a){this.highlight_=a;var b=th 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.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.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.comment?(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.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.inDB_=!1;this.hidden_=!this.db_};Blockly.utils.object.inherits(Blockly.RenderedConnection,Blockly.Connection); -Blockly.RenderedConnection.prototype.dispose=function(){Blockly.RenderedConnection.superClass_.dispose.call(this);this.inDB_&&this.db_.removeConnection_(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()}}};Blockly.RenderedConnection.prototype.moveTo=function(a,b){this.inDB_&&this.db_.removeConnection_(this);this.x_=a;this.y_=b;this.hidden_||this.db_.addConnection(this)};Blockly.RenderedConnection.prototype.moveBy=function(a,b){this.moveTo(this.x_+a,this.y_+b)};Blockly.RenderedConnection.prototype.moveToOffset=function(a){this.moveTo(a.x+this.offsetInBlock_.x,a.y+this.offsetInBlock_.y)}; -Blockly.RenderedConnection.prototype.setOffsetInBlock=function(a,b){this.offsetInBlock_.x=a;this.offsetInBlock_.y=b};Blockly.RenderedConnection.prototype.getOffsetInBlock=function(){return this.offsetInBlock_}; -Blockly.RenderedConnection.prototype.tighten_=function(){var a=this.targetConnection.x_-this.x_,b=this.targetConnection.y_-this.y_;if(0!=a||0!=b){var c=this.targetBlock(),d=c.getSvgRoot();if(!d)throw Error("block is not rendered.");d=Blockly.utils.getRelativeXY(d);c.getSvgRoot().setAttribute("transform","translate("+(d.x-a)+","+(d.y-b)+")");c.moveConnections_(-a,-b)}};Blockly.RenderedConnection.prototype.closest=function(a,b){return this.dbOpposite_.searchForClosest(this,a,b)}; -Blockly.RenderedConnection.prototype.highlight=function(){var a=this.sourceBlock_.workspace.getRenderer().getConstants();a=this.type==Blockly.INPUT_VALUE||this.type==Blockly.OUTPUT_VALUE?Blockly.utils.svgPaths.moveBy(0,-5)+Blockly.utils.svgPaths.lineOnAxis("v",5)+a.PUZZLE_TAB.pathDown+Blockly.utils.svgPaths.lineOnAxis("v",5):Blockly.utils.svgPaths.moveBy(-5,0)+Blockly.utils.svgPaths.lineOnAxis("h",5)+a.NOTCH.pathLeft+Blockly.utils.svgPaths.lineOnAxis("h",5);var b=this.sourceBlock_.getRelativeToSurfaceXY(); -Blockly.Connection.highlightedPath_=Blockly.utils.dom.createSvgElement("path",{"class":"blocklyHighlightedConnectionPath",d:a,transform:"translate("+(this.x_-b.x)+","+(this.y_-b.y)+")"+(this.sourceBlock_.RTL?" scale(-1 1)":"")},this.sourceBlock_.getSvgRoot())}; -Blockly.RenderedConnection.prototype.unhideAll=function(){this.setHidden(!1);var a=[];if(this.type!=Blockly.INPUT_VALUE&&this.type!=Blockly.NEXT_STATEMENT)return a;var b=this.targetBlock();if(b){if(b.isCollapsed()){var c=[];b.outputConnection&&c.push(b.outputConnection);b.nextConnection&&c.push(b.nextConnection);b.previousConnection&&c.push(b.previousConnection)}else c=b.getConnections_(!0);for(var d=0;db?!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.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)}; +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)}; +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()}}}; +Blockly.RenderedConnection.prototype.moveTo=function(a,b){this.trackedState_==Blockly.RenderedConnection.TrackedState.WILL_TRACK?(this.db_.addConnection(this,b),this.trackedState_=Blockly.RenderedConnection.TrackedState.TRACKED):this.trackedState_==Blockly.RenderedConnection.TrackedState.TRACKED&&(this.db_.removeConnection(this,this.y),this.db_.addConnection(this,b));this.x=a;this.y=b};Blockly.RenderedConnection.prototype.moveBy=function(a,b){this.moveTo(this.x+a,this.y+b)}; +Blockly.RenderedConnection.prototype.moveToOffset=function(a){this.moveTo(a.x+this.offsetInBlock_.x,a.y+this.offsetInBlock_.y)};Blockly.RenderedConnection.prototype.setOffsetInBlock=function(a,b){this.offsetInBlock_.x=a;this.offsetInBlock_.y=b};Blockly.RenderedConnection.prototype.getOffsetInBlock=function(){return this.offsetInBlock_}; +Blockly.RenderedConnection.prototype.tighten=function(){var a=this.targetConnection.x-this.x,b=this.targetConnection.y-this.y;if(0!=a||0!=b){var c=this.targetBlock(),d=c.getSvgRoot();if(!d)throw Error("block is not rendered.");d=Blockly.utils.getRelativeXY(d);c.getSvgRoot().setAttribute("transform","translate("+(d.x-a)+","+(d.y-b)+")");c.moveConnections(-a,-b)}};Blockly.RenderedConnection.prototype.closest=function(a,b){return this.dbOpposite_.searchForClosest(this,a,b)}; +Blockly.RenderedConnection.prototype.highlight=function(){var a=this.sourceBlock_.workspace.getRenderer().getConstants();var b=a.shapeFor(this);this.type==Blockly.INPUT_VALUE||this.type==Blockly.OUTPUT_VALUE?(a=a.TAB_OFFSET_FROM_TOP,b=Blockly.utils.svgPaths.moveBy(0,-a)+Blockly.utils.svgPaths.lineOnAxis("v",a)+b.pathDown+Blockly.utils.svgPaths.lineOnAxis("v",a)):(a=a.NOTCH_OFFSET_LEFT-a.CORNER_RADIUS,b=Blockly.utils.svgPaths.moveBy(-a,0)+Blockly.utils.svgPaths.lineOnAxis("h",a)+b.pathLeft+Blockly.utils.svgPaths.lineOnAxis("h", +a));a=this.sourceBlock_.getRelativeToSurfaceXY();Blockly.Connection.highlightedPath_=Blockly.utils.dom.createSvgElement("path",{"class":"blocklyHighlightedConnectionPath",d:b,transform:"translate("+(this.x-a.x)+","+(this.y-a.y)+")"+(this.sourceBlock_.RTL?" scale(-1 1)":"")},this.sourceBlock_.getSvgRoot())};Blockly.RenderedConnection.prototype.unhighlight=function(){Blockly.utils.dom.removeNode(Blockly.Connection.highlightedPath_);delete Blockly.Connection.highlightedPath_}; +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.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.onCheckChanged_=function(){this.isConnected()&&!this.checkType_(this.targetConnection)&&((this.isSuperior()?this.targetBlock():this.sourceBlock_).unplug(),this.sourceBlock_.bumpNeighbours())};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.Icon=function(a){this.block_=a};Blockly.Icon.prototype.collapseHidden=!0;Blockly.Icon.prototype.SIZE=17;Blockly.Icon.prototype.bubble_=null;Blockly.Icon.prototype.iconXY_=null; -Blockly.Icon.prototype.createIcon=function(){this.iconGroup_||(this.iconGroup_=Blockly.utils.dom.createSvgElement("g",{"class":"blocklyIconGroup"},null),this.block_.isInFlyout&&Blockly.utils.dom.addClass(this.iconGroup_,"blocklyIconGroupReadonly"),this.drawIcon_(this.iconGroup_),this.block_.getSvgRoot().appendChild(this.iconGroup_),Blockly.bindEventWithChecks_(this.iconGroup_,"mouseup",this,this.iconClick_),this.updateEditable())}; -Blockly.Icon.prototype.dispose=function(){Blockly.utils.dom.removeNode(this.iconGroup_);this.iconGroup_=null;this.setVisible(!1);this.block_=null};Blockly.Icon.prototype.updateEditable=function(){};Blockly.Icon.prototype.isVisible=function(){return!!this.bubble_};Blockly.Icon.prototype.iconClick_=function(a){this.block_.workspace.isDragging()||this.block_.isInFlyout||Blockly.utils.isRightButton(a)||this.setVisible(!this.isVisible())}; -Blockly.Icon.prototype.updateColour=function(){this.isVisible()&&this.bubble_.setColour(this.block_.getColour())};Blockly.Icon.prototype.setIconLocation=function(a){this.iconXY_=a;this.isVisible()&&this.bubble_.setAnchorLocation(a)}; -Blockly.Icon.prototype.computeIconLocation=function(){var a=this.block_.getRelativeToSurfaceXY(),b=Blockly.utils.getRelativeXY(this.iconGroup_);a=new Blockly.utils.Coordinate(a.x+b.x+this.SIZE/2,a.y+b.y+this.SIZE/2);Blockly.utils.Coordinate.equals(this.getIconLocation(),a)||this.setIconLocation(a)};Blockly.Icon.prototype.getIconLocation=function(){return this.iconXY_}; -Blockly.Icon.prototype.getCorrectedSize=function(){return new Blockly.utils.Size(Blockly.Icon.prototype.SIZE,Blockly.Icon.prototype.SIZE-2)};Blockly.Warning=function(a){Blockly.Warning.superClass_.constructor.call(this,a);this.createIcon();this.text_={}};Blockly.utils.object.inherits(Blockly.Warning,Blockly.Icon);Blockly.Warning.prototype.collapseHidden=!1; -Blockly.Warning.prototype.drawIcon_=function(a){Blockly.utils.dom.createSvgElement("path",{"class":"blocklyIconShape",d:"M2,15Q-1,15 0.5,12L6.5,1.7Q8,-1 9.5,1.7L15.5,12Q17,15 14,15z"},a);Blockly.utils.dom.createSvgElement("path",{"class":"blocklyIconSymbol",d:"m7,4.8v3.16l0.27,2.27h1.46l0.27,-2.27v-3.16z"},a);Blockly.utils.dom.createSvgElement("rect",{"class":"blocklyIconSymbol",x:"7",y:"11",height:"2",width:"2"},a)}; -Blockly.Warning.textToDom_=function(a){var b=Blockly.utils.dom.createSvgElement("text",{"class":"blocklyText blocklyBubbleText",y:Blockly.Bubble.BORDER_WIDTH},null);a=a.split("\n");for(var c=0;c=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.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>>/g,d);d=document.createElement("style");c=document.createTextNode(c);d.appendChild(c);document.head.insertBefore(d,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")}; -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: fixed;","left: 0;","top: 0;","z-index: 1000;","display: none;","border: 1px solid;","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 {","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;","}",".arrowTop {","border-top: 1px solid;","border-left: 1px solid;","border-top-left-radius: 4px;", -"border-color: inherit;","}",".arrowBottom {","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>.blocklyPath {","stroke: #fc3;","stroke-width: 3px;","}",".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","}",".blocklyReplaceable .blocklyPath {","fill-opacity: .5;","}",".blocklyReplaceable .blocklyPathLight,",".blocklyReplaceable .blocklyPathDark {","display: none;","}",".blocklyText {","cursor: default;","fill: #fff;","font-family: sans-serif;", -"font-size: 11pt;","}",".blocklyMultilineText {","font-family: monospace;","}",".blocklyNonEditableText>text {","pointer-events: none;","}",".blocklyNonEditableText>rect,",".blocklyEditableText>rect {","fill: #fff;","fill-opacity: .6;","}",".blocklyNonEditableText>text,",".blocklyEditableText>text {","fill: #000;","}",".blocklyEditableText:hover>rect {","stroke: #fff;","stroke-width: 2;","}",".blocklyBubbleText {","fill: #000;","}",".blocklyFlyout {","position: absolute;","z-index: 20;","}",".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;", -"}",".blocklyCommentForeignObject {","position: relative;","z-index: 0;","}",".blocklyCommentRect {","fill: #E7DE8E;","stroke: #bcA903;","stroke-width: 1px","}",".blocklyCommentTarget {","fill: transparent;","stroke: #bcA903;","}",".blocklyCommentTargetFocused {","fill: none;","}",".blocklyCommentHandleTarget {","fill: none;","}",".blocklyCommentHandleTargetFocused {","fill: transparent;","}",".blocklyFocused>.blocklyCommentRect {","fill: #B9B272;","stroke: #B9B272;","}",".blocklySelected>.blocklyCommentTarget {", -"stroke: #fc3;","stroke-width: 3px;","}",".blocklyCommentTextarea {","background-color: #fef49c;","border: 0;","outline: 0;","margin: 0;","padding: 3px;","resize: none;","display: block;","overflow: hidden;","}",".blocklyCommentDeleteIcon {","cursor: pointer;","fill: #000;","display: none","}",".blocklySelected > .blocklyCommentDeleteIcon {","display: block","}",".blocklyDeleteIconShape {","fill: #000;","stroke: #000;","stroke-width: 1px;","}",".blocklyDeleteIconShape.blocklyDeleteIconHighlighted {", -"stroke: #fc3;","}",".blocklyHtmlInput {","border: none;","border-radius: 4px;","font-family: sans-serif;","height: 100%;","margin: 0;","outline: none;","padding: 0;","width: 100%;","text-align: center;","}",".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;","}",".blocklyVerticalCursor {","stroke-width: 3px;","fill: rgba(255,255,255,.5);","}",".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: normal 13px Arial, sans-serif;","}",".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;","}"]; -Blockly.DropDownDiv=function(){};Blockly.DropDownDiv.DIV_=null;Blockly.DropDownDiv.boundsElement_=null;Blockly.DropDownDiv.owner_=null;Blockly.DropDownDiv.positionToField_=null;Blockly.DropDownDiv.ARROW_SIZE=16;Blockly.DropDownDiv.BORDER_SIZE=1;Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING=12;Blockly.DropDownDiv.PADDING_Y=16;Blockly.DropDownDiv.ANIMATION_TIME=.25;Blockly.DropDownDiv.DEFAULT_DROPDOWN_BORDER_COLOR="#dadce0";Blockly.DropDownDiv.DEFAULT_DROPDOWN_COLOR="#fff"; -Blockly.DropDownDiv.animateOutTimer_=null;Blockly.DropDownDiv.onHide_=null; -Blockly.DropDownDiv.createDom=function(){if(!Blockly.DropDownDiv.DIV_){var a=document.createElement("div");a.className="blocklyDropDownDiv";a.style.backgroundColor=Blockly.DropDownDiv.DEFAULT_DROPDOWN_COLOR;a.style.borderColor=Blockly.DropDownDiv.DEFAULT_DROPDOWN_BORDER_COLOR;document.body.appendChild(a);Blockly.DropDownDiv.DIV_=a;var b=document.createElement("div");b.className="blocklyDropDownContent";a.appendChild(b);Blockly.DropDownDiv.content_=b;b=document.createElement("div");b.className="blocklyDropDownArrow"; -a.appendChild(b);Blockly.DropDownDiv.arrow_=b;Blockly.DropDownDiv.DIV_.style.opacity=0;Blockly.DropDownDiv.DIV_.style.transition="transform "+Blockly.DropDownDiv.ANIMATION_TIME+"s, opacity "+Blockly.DropDownDiv.ANIMATION_TIME+"s";a.addEventListener("focusin",function(){Blockly.utils.dom.addClass(a,"focused")});a.addEventListener("focusout",function(){Blockly.utils.dom.removeClass(a,"focused")})}};Blockly.DropDownDiv.setBoundsElement=function(a){Blockly.DropDownDiv.boundsElement_=a}; -Blockly.DropDownDiv.getContentDiv=function(){return Blockly.DropDownDiv.content_};Blockly.DropDownDiv.clearContent=function(){Blockly.DropDownDiv.content_.innerHTML="";Blockly.DropDownDiv.content_.style.width=""};Blockly.DropDownDiv.setColour=function(a,b){Blockly.DropDownDiv.DIV_.style.backgroundColor=a;Blockly.DropDownDiv.DIV_.style.borderColor=b};Blockly.DropDownDiv.setCategory=function(a){Blockly.DropDownDiv.DIV_.setAttribute("data-category",a)}; -Blockly.DropDownDiv.showPositionedByBlock=function(a,b,c,d){var e=b.workspace.scale,f=b.width,g=b.height;f*=e;g*=e;e=b.getSvgRoot().getBoundingClientRect();f=e.left+f/2;g=e.top+g;e=e.top;d&&(e+=d);Blockly.DropDownDiv.setBoundsElement(b.workspace.getParentSvg().parentNode);return Blockly.DropDownDiv.show(a,b.RTL,f,g,f,e,c)}; -Blockly.DropDownDiv.showPositionedByField=function(a,b,c){var d=a.getSvgRoot().getBoundingClientRect(),e=d.left+d.width/2,f=d.bottom;d=d.top;c&&(d+=c);c=a.getSourceBlock();Blockly.DropDownDiv.positionToField_=!0;Blockly.DropDownDiv.setBoundsElement(c.workspace.getParentSvg().parentNode);return Blockly.DropDownDiv.show(a,c.RTL,e,f,e,d,b)}; -Blockly.DropDownDiv.show=function(a,b,c,d,e,f,g){Blockly.DropDownDiv.owner_=a;Blockly.DropDownDiv.onHide_=g||null;a=Blockly.DropDownDiv.getPositionMetrics(c,d,e,f);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 arrowTop":"blocklyDropDownArrow arrowBottom")):Blockly.DropDownDiv.arrow_.style.display="none"; -Blockly.DropDownDiv.DIV_.style.direction=b?"rtl":"ltr";Blockly.DropDownDiv.positionInternal_(a.initialX,a.initialY,a.finalX,a.finalY);return a.arrowAtTop};Blockly.DropDownDiv.getBoundsInfo_=function(){var a=Blockly.DropDownDiv.boundsElement_.getBoundingClientRect(),b=Blockly.utils.style.getSize(Blockly.DropDownDiv.boundsElement_);return{left:a.left,right:a.left+b.width,top:a.top,bottom:a.top+b.height,width:b.width,height:b.height}}; -Blockly.DropDownDiv.getPositionMetrics=function(a,b,c,d){var e=Blockly.DropDownDiv.getBoundsInfo_(),f=Blockly.utils.style.getSize(Blockly.DropDownDiv.DIV_);return b+f.heighte.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.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=Blockly.DropDownDiv.DEFAULT_DROPDOWN_COLOR;a.style.borderColor=Blockly.DropDownDiv.DEFAULT_DROPDOWN_BORDER_COLOR;Blockly.DropDownDiv.onHide_&&(Blockly.DropDownDiv.onHide_(),Blockly.DropDownDiv.onHide_= -null);Blockly.DropDownDiv.clearContent();Blockly.DropDownDiv.owner_=null}};Blockly.DropDownDiv.positionInternal_=function(a,b,c,d){a=Math.floor(a);b=Math.floor(b);c=Math.floor(c);d=Math.floor(d);var e=Blockly.DropDownDiv.DIV_;e.style.left=a+"px";e.style.top=b+"px";e.style.display="block";e.style.opacity=1;e.style.transform="translate("+(c-a)+"px,"+(d-b)+"px)"}; -Blockly.DropDownDiv.repositionForWindowResize=function(){if(Blockly.DropDownDiv.owner_){var a=Blockly.DropDownDiv.owner_.getSourceBlock(),b=a.workspace.scale,c=Blockly.DropDownDiv.positionToField_?Blockly.DropDownDiv.owner_.size_.width:a.width,d=Blockly.DropDownDiv.positionToField_?Blockly.DropDownDiv.owner_.size_.height:a.height;c*=b;d*=b;a=Blockly.DropDownDiv.positionToField_?Blockly.DropDownDiv.owner_.fieldGroup_.getBoundingClientRect():a.getSvgRoot().getBoundingClientRect();c=a.left+c/2;d=Blockly.DropDownDiv.getPositionMetrics(c, -a.top+d,c,a.top);Blockly.DropDownDiv.positionInternal_(d.initialX,d.initialY,d.finalX,d.finalY)}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);0 document.");}else a=null;return a};Blockly.WorkspaceDragSurfaceSvg=function(a){this.container_=a;this.createDom()};Blockly.WorkspaceDragSurfaceSvg.prototype.SVG_=null;Blockly.WorkspaceDragSurfaceSvg.prototype.dragGroup_=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){var c=a.toFixed(0),d=b.toFixed(0);this.SVG_.style.display="block";Blockly.utils.dom.setCssTransform(this.SVG_,"translate3d("+c+"px, "+d+"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.blockRendering.rendererMap_={};Blockly.blockRendering.useDebugger=!1;Blockly.blockRendering.register=function(a,b){if(Blockly.blockRendering.rendererMap_[a])throw Error("Renderer has already been registered.");Blockly.blockRendering.rendererMap_[a]=b};Blockly.blockRendering.unregister=function(a){Blockly.blockRendering.rendererMap_[a]?delete Blockly.blockRendering.rendererMap_[a]:console.warn('No renderer mapping for name "'+a+'" found to unregister')}; -Blockly.blockRendering.startDebugger=function(){Blockly.blockRendering.useDebugger=!0};Blockly.blockRendering.stopDebugger=function(){Blockly.blockRendering.useDebugger=!1};Blockly.blockRendering.init=function(a){if(!Blockly.blockRendering.rendererMap_[a])throw Error("Renderer not registered: ",a);var b=function(){b.superClass_.constructor.call(this)};Blockly.utils.object.inherits(b,Blockly.blockRendering.rendererMap_[a]);a=new b;a.init();return a};Blockly.ConnectionDB=function(){this.connections_=[]};Blockly.ConnectionDB.prototype.addConnection=function(a){if(a.inDB_)throw Error("Connection already in database.");if(!a.getSourceBlock().isInFlyout){var b=this.findPositionForConnection_(a);this.connections_.splice(b,0,a);a.inDB_=!0}}; -Blockly.ConnectionDB.prototype.findConnection=function(a){if(!this.connections_.length)return-1;var b=this.findPositionForConnection_(a);if(b>=this.connections_.length)return-1;for(var c=a.y_,d=b;0<=d&&this.connections_[d].y_==c;){if(this.connections_[d]==a)return d;d--}for(;ba.y_)c=d;else{b=d;break}}return b}; -Blockly.ConnectionDB.prototype.removeConnection_=function(a){if(!a.inDB_)throw Error("Connection not in database.");var b=this.findConnection(a);if(-1==b)throw Error("Unable to find connection in connectionDB.");a.inDB_=!1;this.connections_.splice(b,1)}; -Blockly.ConnectionDB.prototype.getNeighbours=function(a,b){function c(a){var c=e-d[a].x_,g=f-d[a].y_;Math.sqrt(c*c+g*g)<=b&&l.push(d[a]);return g=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 ga)throw Error("Cannot unsubscribe a workspace that hasn't been subscribed.");this.subscribedWorkspaces_.splice(a,1)}; +Blockly.ThemeManager.prototype.subscribe=function(a,b,c){this.componentDB_[b]||(this.componentDB_[b]=[]);this.componentDB_[b].push({element:a,propertyName:c});b=this.theme_&&this.theme_.getComponentStyle(b);a.style[c]=b||""};Blockly.ThemeManager.prototype.unsubscribe=function(a){if(a)for(var b=Object.keys(this.componentDB_),c=0,d;d=b[c];c++){for(var e=this.componentDB_[d],f=e.length-1;0<=f;f--)e[f].element===a&&e.splice(f,1);this.componentDB_[d].length||delete this.componentDB_[d]}}; +Blockly.ThemeManager.prototype.dispose=function(){this.componentDB_=this.subscribedWorkspaces_=this.theme_=this.owner_=null};Blockly.TouchGesture=function(a,b){Blockly.TouchGesture.superClass_.constructor.call(this,a,b);this.isMultiTouch_=!1;this.cachedPoints_=Object.create(null);this.startDistance_=this.previousScale_=0;this.isPinchZoomEnabled_=this.onStartWrapper_=null};Blockly.utils.object.inherits(Blockly.TouchGesture,Blockly.Gesture);Blockly.TouchGesture.ZOOM_IN_MULTIPLIER=5;Blockly.TouchGesture.ZOOM_OUT_MULTIPLIER=6; +Blockly.TouchGesture.prototype.doStart=function(a){this.isPinchZoomEnabled_=this.startWorkspace_.options.zoomOptions&&this.startWorkspace_.options.zoomOptions.pinch;Blockly.TouchGesture.superClass_.doStart.call(this,a);!this.isEnding_&&Blockly.Touch.isTouchEvent(a)&&this.handleTouchStart(a)}; Blockly.TouchGesture.prototype.bindMouseEvents=function(a){this.onStartWrapper_=Blockly.bindEventWithChecks_(document,"mousedown",null,this.handleStart.bind(this),!0);this.onMoveWrapper_=Blockly.bindEventWithChecks_(document,"mousemove",null,this.handleMove.bind(this),!0);this.onUpWrapper_=Blockly.bindEventWithChecks_(document,"mouseup",null,this.handleUp.bind(this),!0);a.preventDefault();a.stopPropagation()}; Blockly.TouchGesture.prototype.handleStart=function(a){!this.isDragging()&&Blockly.Touch.isTouchEvent(a)&&(this.handleTouchStart(a),this.isMultiTouch()&&Blockly.longStop_())};Blockly.TouchGesture.prototype.handleMove=function(a){this.isDragging()?Blockly.Touch.shouldHandleEvent(a)&&Blockly.TouchGesture.superClass_.handleMove.call(this,a):this.isMultiTouch()?(Blockly.Touch.isTouchEvent(a)&&this.handleTouchMove(a),Blockly.longStop_()):Blockly.TouchGesture.superClass_.handleMove.call(this,a)}; Blockly.TouchGesture.prototype.handleUp=function(a){Blockly.Touch.isTouchEvent(a)&&!this.isDragging()&&this.handleTouchEnd(a);!this.isMultiTouch()||this.isDragging()?Blockly.Touch.shouldHandleEvent(a)&&Blockly.TouchGesture.superClass_.handleUp.call(this,a):(a.preventDefault(),a.stopPropagation(),this.dispose())};Blockly.TouchGesture.prototype.isMultiTouch=function(){return this.isMultiTouch_}; Blockly.TouchGesture.prototype.dispose=function(){Blockly.TouchGesture.superClass_.dispose.call(this);this.onStartWrapper_&&Blockly.unbindEvent_(this.onStartWrapper_)};Blockly.TouchGesture.prototype.handleTouchStart=function(a){var b=Blockly.Touch.getTouchIdentifierFromEvent(a);this.cachedPoints_[b]=this.getTouchPoint(a);b=Object.keys(this.cachedPoints_);2==b.length&&(this.startDistance_=Blockly.utils.Coordinate.distance(this.cachedPoints_[b[0]],this.cachedPoints_[b[1]]),this.isMultiTouch_=!0,a.preventDefault())}; -Blockly.TouchGesture.prototype.handleTouchMove=function(a){var b=Blockly.Touch.getTouchIdentifierFromEvent(a);this.cachedPoints_[b]=this.getTouchPoint(a);b=Object.keys(this.cachedPoints_);if(2==b.length){b=this.touchScale_=Blockly.utils.Coordinate.distance(this.cachedPoints_[b[0]],this.cachedPoints_[b[1]])/this.startDistance_;if(0this.previousScale_){var c=b-this.previousScale_;c=0Object.keys(this.cachedPoints_).length&&(this.cachedPoints_={},this.previousScale_=0)}; -Blockly.TouchGesture.prototype.getTouchPoint=function(a){return this.startWorkspace_?new Blockly.utils.Coordinate(a.pageX?a.pageX:a.changedTouches[0].pageX,a.pageY?a.pageY:a.changedTouches[0].pageY):null};Blockly.WorkspaceAudio=function(a){this.parentWorkspace_=a;this.SOUNDS_=Object.create(null)};Blockly.WorkspaceAudio.prototype.lastSound_=null;Blockly.WorkspaceAudio.prototype.dispose=function(){this.SOUNDS_=this.parentWorkspace_=null}; +Blockly.TouchGesture.prototype.handleTouchMove=function(a){var b=Blockly.Touch.getTouchIdentifierFromEvent(a);this.cachedPoints_[b]=this.getTouchPoint(a);b=Object.keys(this.cachedPoints_);this.isPinchZoomEnabled_&&2===b.length?this.handlePinch_(a):Blockly.TouchGesture.superClass_.handleMove.call(this,a)}; +Blockly.TouchGesture.prototype.handlePinch_=function(a){var b=Object.keys(this.cachedPoints_);b=Blockly.utils.Coordinate.distance(this.cachedPoints_[b[0]],this.cachedPoints_[b[1]])/this.startDistance_;if(0this.previousScale_){var c=b-this.previousScale_;c=0Object.keys(this.cachedPoints_).length&&(this.cachedPoints_=Object.create(null),this.previousScale_=0)};Blockly.TouchGesture.prototype.getTouchPoint=function(a){return this.startWorkspace_?new Blockly.utils.Coordinate(a.pageX?a.pageX:a.changedTouches[0].pageX,a.pageY?a.pageY:a.changedTouches[0].pageY):null};Blockly.WorkspaceAudio=function(a){this.parentWorkspace_=a;this.SOUNDS_=Object.create(null)};Blockly.WorkspaceAudio.prototype.lastSound_=null;Blockly.WorkspaceAudio.prototype.dispose=function(){this.SOUNDS_=this.parentWorkspace_=null}; Blockly.WorkspaceAudio.prototype.load=function(a,b){if(a.length){try{var c=new Blockly.utils.global.Audio}catch(h){return}for(var d,e=0;e=this.remainingCapacity()||(this.currentGesture_&&this.currentGesture_.cancel(),"comment"==a.tagName.toLowerCase()?this.pasteWorkspaceComment_(a):this.pasteBlock_(a))}; -Blockly.WorkspaceSvg.prototype.pasteBlock_=function(a){Blockly.Events.disable();try{var b=Blockly.Xml.domToBlock(a,this),c=this.getMarker().getCurNode();if(Blockly.keyboardAccessibilityMode&&c){Blockly.navigation.insertBlock(b,c.getLocation());return}var d=parseInt(a.getAttribute("x"),10),e=parseInt(a.getAttribute("y"),10);if(!isNaN(d)&&!isNaN(e)){this.RTL&&(d=-d);do{a=!1;var f=this.getAllBlocks(!1);c=0;for(var g;g=f[c];c++){var h=g.getRelativeToSurfaceXY();if(1>=Math.abs(d-h.x)&&1>=Math.abs(e-h.y)){a= -!0;break}}if(!a){var k=b.getConnections_(!1);c=0;for(var l;l=k[c];c++)if(l.closest(Blockly.SNAP_RADIUS,new Blockly.utils.Coordinate(d,e)).connection){a=!0;break}}a&&(d=this.RTL?d-Blockly.SNAP_RADIUS:d+Blockly.SNAP_RADIUS,e+=2*Blockly.SNAP_RADIUS)}while(a);b.moveBy(d,e)}}finally{Blockly.Events.enable()}Blockly.Events.isEnabled()&&!b.isShadow()&&Blockly.Events.fire(new Blockly.Events.BlockCreate(b));b.select()}; +Blockly.WorkspaceSvg.prototype.pasteBlock_=function(a){Blockly.Events.disable();try{var b=Blockly.Xml.domToBlock(a,this),c=this.getMarker(Blockly.navigation.MARKER_NAME).getCurNode();if(this.keyboardAccessibilityMode&&c&&c.isConnection()){var d=c.getLocation();Blockly.navigation.insertBlock(b,d);return}var e=parseInt(a.getAttribute("x"),10),f=parseInt(a.getAttribute("y"),10);if(!isNaN(e)&&!isNaN(f)){this.RTL&&(e=-e);do{a=!1;var g=this.getAllBlocks(!1);c=0;for(var h;h=g[c];c++){var k=h.getRelativeToSurfaceXY(); +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_.flyout_&&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.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.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){var c=Blockly.utils.mouseToSvg(a,this.getParentSvg(),this.getInverseScreenCTM());c.x/=this.scale;c.y/=this.scale;this.dragDeltaXY_=Blockly.utils.Coordinate.difference(b,c)};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.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}; -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}; +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}; -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+Blockly.BlockSvg.MIN_BLOCK_Y}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);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()}catch(a){try{this.getParentSvg().parentNode.setActive()}catch(b){this.getParentSvg().parentNode.focus()}}}; -Blockly.WorkspaceSvg.prototype.zoom=function(a,b,c){if(!this.isFlyout&&!this.isMutator){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?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.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||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_();b.languageTree&&(a.toolbox_?a.toolbox_.init(a):a.flyout_&&(a.flyout_.init(a),a.flyout_.show(b.languageTree.childNodes),a.flyout_.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, +Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_=function(a){var b=this.getMetrics();"number"==typeof a.x&&(this.scrollX=-b.contentWidth*a.x-b.contentLeft);"number"==typeof a.y&&(this.scrollY=-b.contentHeight*a.y-b.contentTop);this.translate(this.scrollX+b.absoluteLeft,this.scrollY+b.absoluteTop)};Blockly.WorkspaceSvg.prototype.getBlockById=function(a){return Blockly.WorkspaceSvg.superClass_.getBlockById.call(this,a)}; +Blockly.WorkspaceSvg.prototype.getTopBlocks=function(a){return Blockly.WorkspaceSvg.superClass_.getTopBlocks.call(this,a)};Blockly.WorkspaceSvg.prototype.setResizesEnabled=function(a){var b=!this.resizesEnabled_&&a;this.resizesEnabled_=a;b&&this.resizeContents()};Blockly.WorkspaceSvg.prototype.clear=function(){this.setResizesEnabled(!1);Blockly.WorkspaceSvg.superClass_.clear.call(this);this.setResizesEnabled(!0)}; +Blockly.WorkspaceSvg.prototype.registerButtonCallback=function(a,b){if("function"!=typeof b)throw TypeError("Button callbacks must be functions.");this.flyoutButtonCallbacks_[a]=b};Blockly.WorkspaceSvg.prototype.getButtonCallback=function(a){return(a=this.flyoutButtonCallbacks_[a])?a:null};Blockly.WorkspaceSvg.prototype.removeButtonCallback=function(a){this.flyoutButtonCallbacks_[a]=null}; +Blockly.WorkspaceSvg.prototype.registerToolboxCategoryCallback=function(a,b){if("function"!=typeof b)throw TypeError("Toolbox category callbacks must be functions.");this.toolboxCategoryCallbacks_[a]=b};Blockly.WorkspaceSvg.prototype.getToolboxCategoryCallback=function(a){return this.toolboxCategoryCallbacks_[a]||null};Blockly.WorkspaceSvg.prototype.removeToolboxCategoryCallback=function(a){this.toolboxCategoryCallbacks_[a]=null}; +Blockly.WorkspaceSvg.prototype.getGesture=function(a){var b="mousedown"==a.type||"touchstart"==a.type||"pointerdown"==a.type,c=this.currentGesture_;return c?b&&c.hasStarted()?(console.warn("Tried to start the same gesture twice."),c.cancel(),null):c:b?this.currentGesture_=new Blockly.TouchGesture(a,this):null};Blockly.WorkspaceSvg.prototype.clearGesture=function(){this.currentGesture_=null};Blockly.WorkspaceSvg.prototype.cancelCurrentGesture=function(){this.currentGesture_&&this.currentGesture_.cancel()}; +Blockly.WorkspaceSvg.prototype.getAudioManager=function(){return this.audioManager_};Blockly.WorkspaceSvg.prototype.getGrid=function(){return this.grid_};Blockly.inject=function(a,b){Blockly.checkBlockColourConstants();"string"==typeof a&&(a=document.getElementById(a)||document.querySelector(a));if(!a||!Blockly.utils.dom.containsNode(document,a))throw Error("Error: container is not in current document.");b=new Blockly.Options(b||{});var c=document.createElement("div");c.className="injectionDiv";c.tabIndex=0;Blockly.utils.aria.setState(c,Blockly.utils.aria.State.LABEL,Blockly.Msg.WORKSPACE_ARIA_LABEL);a.appendChild(c);a=Blockly.createDom_(c,b);var d= +new Blockly.BlockDragSurfaceSvg(c),e=new Blockly.WorkspaceDragSurfaceSvg(c),f=Blockly.createMainWorkspace_(a,b,d,e);Blockly.user.keyMap.setKeyMap(b.keyMap);Blockly.init_(f);Blockly.mainWorkspace=f;Blockly.svgResize(f);c.addEventListener("focusin",function(){Blockly.mainWorkspace=f});return f}; +Blockly.createDom_=function(a,b){a.setAttribute("dir","LTR");Blockly.Component.defaultRightToLeft=b.RTL;Blockly.Css.inject(b.hasCss,b.pathToMedia);a=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":"blocklySvg",tabindex:"0"},a);var c=Blockly.utils.dom.createSvgElement("defs",{},a),d=String(Math.random()).substring(2);b.gridPattern=Blockly.Grid.createDom(d,b.gridOptions,c); +return a}; +Blockly.createMainWorkspace_=function(a,b,c,d){b.parentWorkspace=null;var e=new Blockly.WorkspaceSvg(b,c,d);b=e.options;e.scale=b.zoomOptions.startScale;a.appendChild(e.createDom("blocklyMainBackground"));Blockly.utils.dom.addClass(e.getInjectionDiv(),(b.renderer||"geras")+"-renderer");Blockly.utils.dom.addClass(e.getInjectionDiv(),e.getTheme().name+"-theme");!b.hasCategories&&b.languageTree&&(c=e.addFlyout("svg"),Blockly.utils.dom.insertAfter(c,a));b.hasTrashcan&&e.addTrashcan();b.zoomOptions&&b.zoomOptions.controls&& +e.addZoomControls();e.getThemeManager().subscribe(a,"workspaceBackgroundColour","background-color");e.translate(0,0);b.readOnly||e.isMovable()||e.addChangeListener(function(a){if(!e.isDragging()&&!e.isMovable()&&-1!=Blockly.Events.BUMP_EVENTS.indexOf(a.type)){var b=Object.create(null),c=e.getMetrics(),d=e.scale;b.RTL=e.RTL;b.viewLeft=c.viewLeft/d;b.viewTop=c.viewTop/d;b.viewRight=(c.viewLeft+c.viewWidth)/d;b.viewBottom=(c.viewTop+c.viewHeight)/d;e.isContentBounded()?(c=e.getBlocksBoundingBox(),b.contentLeft= +c.left,b.contentTop=c.top,b.contentRight=c.right,b.contentBottom=c.bottom):(b.contentLeft=c.contentLeft/d,b.contentTop=c.contentTop/d,b.contentRight=(c.contentLeft+c.contentWidth)/d,b.contentBottom=(c.contentTop+c.contentHeight)/d);if(b.contentTopb.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; +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}; -Blockly.inject.loadSounds_=function(a,b){var c=b.getAudioManager();c.load([a+"click.mp3",a+"click.wav",a+"click.ogg"],"click");c.load([a+"disconnect.wav",a+"disconnect.mp3",a+"disconnect.ogg"],"disconnect");c.load([a+"delete.mp3",a+"delete.ogg",a+"delete.wav"],"delete");var d=[],e=function(){for(;d.length;)Blockly.unbindEvent_(d.pop());c.preload()};d.push(Blockly.bindEventWithChecks_(document,"mousemove",null,e,!0));d.push(Blockly.bindEventWithChecks_(document,"touchstart",null,e,!0))};Blockly.Action=function(a,b){this.name=a;this.desc=b};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.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 new Blockly.ASTNode(Blockly.ASTNode.types.FIELD,a)}; -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?new Blockly.ASTNode(Blockly.ASTNode.types.INPUT,a.connection):null};Blockly.ASTNode.createBlockNode=function(a){return new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK,a)};Blockly.ASTNode.createStackNode=function(a){return new Blockly.ASTNode(Blockly.ASTNode.types.STACK,a)};Blockly.ASTNode.createWorkspaceNode=function(a,b){return new Blockly.ASTNode(Blockly.ASTNode.types.WORKSPACE,a,{wsCoordinate:b})}; -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.findPreviousEditableField_=function(a,b,c){b=b.fieldRow;a=b.indexOf(a);for(c=(c?b.length:a)-1;a=b[c];c--)if(a.EDITABLE)return b=a,Blockly.ASTNode.createFieldNode(b);return null};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.EDITABLE)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;a1'),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.generateUniqueName=function(a){a=a.getAllVariables();var b="";if(a.length)for(var c=1,d=0,e="ijkmnopqrstuvwxyzabcdefgh".charAt(d);!b;){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.20191014.4";Blockly.mainWorkspace=null;Blockly.selected=null;Blockly.cursor=null;Blockly.keyboardAccessibilityMode=!1;Blockly.draggingConnections_=[];Blockly.clipboardXml_=null;Blockly.clipboardSource_=null;Blockly.clipboardTypeCounts_=null;Blockly.cache3dSupported_=null;Blockly.svgSize=function(a){return{width:a.cachedWidth_,height:a.cachedHeight_}};Blockly.resizeSvgContents=function(a){a.resizeContents()}; +Blockly.Variables.nameUsedWithAnyType_=function(a,b){b=b.getVariableMap().getAllVariables();a=a.toLowerCase();for(var c=0,d;d=b[c];c++)if(d.name.toLowerCase()==a)return d;return null};Blockly.Variables.generateVariableFieldDom=function(a){var b=Blockly.utils.xml.createElement("field");b.setAttribute("name","VAR");b.setAttribute("id",a.getId());b.setAttribute("variabletype",a.type);a=Blockly.utils.xml.createTextNode(a.name);b.appendChild(a);return b}; +Blockly.Variables.getOrCreateVariablePackage=function(a,b,c,d){var e=Blockly.Variables.getVariable(a,b,c,d);e||(e=Blockly.Variables.createVariable_(a,b,c,d));return e};Blockly.Variables.getVariable=function(a,b,c,d){var e=a.getPotentialVariableMap(),f=null;if(b&&(f=a.getVariableById(b),!f&&e&&(f=e.getVariableById(b)),f))return f;if(c){if(void 0==d)throw Error("Tried to look up a variable by name without a type");f=a.getVariable(c,d);!f&&e&&(f=e.getVariable(c,d))}return f}; +Blockly.Variables.createVariable_=function(a,b,c,d){var e=a.getPotentialVariableMap();c||(c=Blockly.Variables.generateUniqueName(a.isFlyout?a.targetWorkspace:a));return e?e.createVariable(c,d,b):a.createVariable(c,d,b)};Blockly.Variables.getAddedVariables=function(a,b){a=a.getAllVariables();var c=[];if(b.length!=a.length)for(var d=0;de?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.20191014.0-develop";Blockly.mainWorkspace=null;Blockly.selected=null;Blockly.draggingConnections=[];Blockly.clipboardXml_=null;Blockly.clipboardSource_=null;Blockly.clipboardTypeCounts_=null;Blockly.cache3dSupported_=null;Blockly.svgSize=function(a){return{width:a.cachedWidth_,height: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(!(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.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.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.toolbox_&&a.toolbox_.flyout_&&a.toolbox_.flyout_.autoClose&&a.toolbox_.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.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.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,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=5;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"));this.width=Blockly.utils.dom.getTextWidth(c);this.height=20;this.isLabel_||(this.width+= +2*Blockly.FlyoutButton.MARGIN,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",this.height-Blockly.FlyoutButton.MARGIN);this.updateTransform_();this.onMouseUpWrapper_=Blockly.bindEventWithChecks_(this.svgGroup_,"mouseup",this,this.onMouseUp_);return this.svgGroup_}; +Blockly.FlyoutButton.prototype.show=function(){this.updateTransform_();this.svgGroup_.setAttribute("display","block")};Blockly.FlyoutButton.prototype.updateTransform_=function(){this.svgGroup_.setAttribute("transform","translate("+this.position_.x+","+this.position_.y+")")};Blockly.FlyoutButton.prototype.moveTo=function(a,b){this.position_.x=a;this.position_.y=b;this.updateTransform_()};Blockly.FlyoutButton.prototype.getPosition=function(){return this.position_}; +Blockly.FlyoutButton.prototype.getTargetWorkspace=function(){return this.targetWorkspace_};Blockly.FlyoutButton.prototype.dispose=function(){this.onMouseUpWrapper_&&Blockly.unbindEvent_(this.onMouseUpWrapper_);this.svgGroup_&&Blockly.utils.dom.removeNode(this.svgGroup_);this.svgText_&&this.workspace_.getThemeManager().unsubscribe(this.svgText_)}; +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;,},.blocklyFlyoutLabelText {,fill: #000;,}".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=this.computeDepth_(),this.setDepth_(a));return a};Blockly.tree.BaseNode.prototype.computeDepth_=function(){var a=this.getParent();return a?a.getDepth()+1:0};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.isRightToLeft()?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.getFirstChild=function(){return this.getChildAt(0)};Blockly.tree.BaseNode.prototype.getLastChild=function(){return this.getChildAt(this.getChildCount()-1)};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.selectFirst=function(){var a=this.getTree();a&&this.firstChild_&&a.setSelectedItem(this.firstChild_)}; @@ -822,225 +883,245 @@ Blockly.tree.BaseNode.prototype.setTreeInternal=function(a){this.tree!=a&&(this. Blockly.tree.TreeNode.prototype.getCalculatedIconClass=function(){var a=this.getExpanded(),b=this.getExpandedIconClass();if(a&&b)return b;b=this.getIconClass();if(!a&&b)return b;b=this.getConfig();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.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.isRightToLeft()?d:c;b[Blockly.utils.KeyCodes.LEFT]=this.isRightToLeft()?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;Blockly.tree.BaseNode.call(this,"",b);this.setExpandedInternal(!0);this.setSelectedInternal(!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.getExpanded=function(){return!0}; -Blockly.tree.TreeControl.prototype.setExpanded=function(a){this.setExpandedInternal(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.getConfig().cssHideRoot}; +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.setExpandedInternal(!0);this.setSelectedInternal(!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.getExpanded=function(){return!0};Blockly.tree.TreeControl.prototype.setExpanded=function(a){this.setExpandedInternal(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.getConfig().cssHideRoot}; Blockly.tree.TreeControl.prototype.getCalculatedIconClass=function(){var a=this.getExpanded(),b=this.getExpandedIconClass();if(a&&b)return b;b=this.getIconClass();if(!a&&b)return b;b=this.getConfig();return a&&b.cssExpandedRootIcon?b.cssTreeIcon+" "+b.cssExpandedRootIcon:""}; 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_.setSelectedInternal(!1);(this.selectedItem_=a)&&a.setSelectedInternal(!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.getConfig().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(){Blockly.unbindEvent_(this.onFocusWrapper_);Blockly.unbindEvent_(this.onBlurWrapper_);Blockly.unbindEvent_(this.onClickWrapper_);Blockly.unbindEvent_(this.onKeydownWrapper_)};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;b=b.parentNode}return null}; -Blockly.tree.TreeControl.prototype.createNode=function(a){return new Blockly.tree.TreeNode(this.toolbox_,a||"",this.getConfig())};Blockly.FieldTextInput=function(a,b,c){this.spellcheck_=!0;null==a&&(a="");Blockly.FieldTextInput.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldTextInput,Blockly.Field);Blockly.FieldTextInput.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldTextInput(b,void 0,a)};Blockly.FieldTextInput.prototype.SERIALIZABLE=!0;Blockly.FieldTextInput.FONTSIZE=11;Blockly.FieldTextInput.BORDERRADIUS=4; -Blockly.FieldTextInput.prototype.CURSOR="text";Blockly.FieldTextInput.prototype.configure_=function(a){Blockly.FieldTextInput.superClass_.configure_.call(this,a);"boolean"==typeof a.spellcheck&&(this.spellcheck_=a.spellcheck)};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.render_=function(){Blockly.FieldTextInput.superClass_.render_.call(this);this.isBeingEdited_&&(this.sourceBlock_.RTL?setTimeout(this.resizeEditor_.bind(this),0):this.resizeEditor_(),this.isTextValid_?(Blockly.utils.dom.removeClass(this.htmlInput_,"blocklyInvalidInput"),Blockly.utils.aria.setState(this.htmlInput_,"invalid",!1)):(Blockly.utils.dom.addClass(this.htmlInput_,"blocklyInvalidInput"),Blockly.utils.aria.setState(this.htmlInput_,"invalid",!0)))}; -Blockly.FieldTextInput.prototype.setSpellcheck=function(a){a!=this.spellcheck_&&(this.spellcheck_=a,this.htmlInput_&&this.htmlInput_.setAttribute("spellcheck",this.spellcheck_))};Blockly.FieldTextInput.prototype.showEditor_=function(a){this.workspace_=this.sourceBlock_.workspace;a=a||!1;!a&&(Blockly.utils.userAgent.MOBILE||Blockly.utils.userAgent.ANDROID||Blockly.utils.userAgent.IPAD)?this.showPromptEditor_():this.showInlineEditor_(a)}; -Blockly.FieldTextInput.prototype.showPromptEditor_=function(){var a=this;Blockly.prompt(Blockly.Msg.CHANGE_VALUE_TITLE,this.getText(),function(b){a.setValue(b)})};Blockly.FieldTextInput.prototype.showInlineEditor_=function(a){Blockly.WidgetDiv.show(this,this.sourceBlock_.RTL,this.widgetDispose_.bind(this));this.htmlInput_=this.widgetCreate_();this.isBeingEdited_=!0;a||(this.htmlInput_.focus(),this.htmlInput_.select())}; -Blockly.FieldTextInput.prototype.widgetCreate_=function(){var a=Blockly.WidgetDiv.DIV,b=document.createElement("input");b.className="blocklyHtmlInput";b.setAttribute("spellcheck",this.spellcheck_);var c=Blockly.FieldTextInput.FONTSIZE*this.workspace_.scale+"pt";a.style.fontSize=c;b.style.fontSize=c;b.style.borderRadius=Blockly.FieldTextInput.BORDERRADIUS*this.workspace_.scale+"px";a.appendChild(b);b.value=b.defaultValue=this.getEditorText_(this.value_);b.untypedDefaultValue_=this.value_;b.oldValue_= -null;Blockly.utils.userAgent.GECKO?setTimeout(this.resizeEditor_.bind(this),0):this.resizeEditor_();this.bindInputEvents_(b);return b};Blockly.FieldTextInput.prototype.widgetDispose_=function(){this.isBeingEdited_=!1;this.isTextValid_=!0;this.forceRerender();if(this.onFinishEditing_)this.onFinishEditing_(this.value_);this.unbindInputEvents_();var a=Blockly.WidgetDiv.DIV.style;a.width="auto";a.height="auto";a.fontSize=""}; -Blockly.FieldTextInput.prototype.bindInputEvents_=function(a){this.onKeyDownWrapper_=Blockly.bindEventWithChecks_(a,"keydown",this,this.onHtmlInputKeyDown_);this.onKeyInputWrapper_=Blockly.bindEventWithChecks_(a,"input",this,this.onHtmlInputChange_)};Blockly.FieldTextInput.prototype.unbindInputEvents_=function(){Blockly.unbindEvent_(this.onKeyDownWrapper_);Blockly.unbindEvent_(this.onKeyInputWrapper_)}; +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;b=b.parentNode}return null};Blockly.tree.TreeControl.prototype.createNode=function(a){return new Blockly.tree.TreeNode(this.toolbox_,a||"",this.getConfig())};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.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,toolboxPosition:a.options.toolboxPosition,renderer:a.options.renderer});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", +a&&a.getText(),b&&b.getText()),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.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.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_.selectFirst()};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: #e4e4e4;","}",".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});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.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; +Blockly.Trashcan.prototype.createDom=function(){this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{"class":"blocklyTrash"},null);var a=String(Math.random()).substring(2);var b=Blockly.utils.dom.createSvgElement("clipPath",{id:"blocklyTrashBodyClipPath"+a},this.svgGroup_);Blockly.utils.dom.createSvgElement("rect",{width:this.WIDTH_,height:this.BODY_HEIGHT_,y:this.LID_HEIGHT_},b);var c=Blockly.utils.dom.createSvgElement("image",{width:Blockly.SPRITE.width,x:-this.SPRITE_LEFT_,height:Blockly.SPRITE.height, +y:-this.SPRITE_TOP_,"clip-path":"url(#blocklyTrashBodyClipPath"+a+")"},this.svgGroup_);c.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.workspace_.options.pathToMedia+Blockly.SPRITE.url);b=Blockly.utils.dom.createSvgElement("clipPath",{id:"blocklyTrashLidClipPath"+a},this.svgGroup_);Blockly.utils.dom.createSvgElement("rect",{width:this.WIDTH_,height:this.LID_HEIGHT_},b);this.svgLid_=Blockly.utils.dom.createSvgElement("image",{width:Blockly.SPRITE.width,x:-this.SPRITE_LEFT_,height:Blockly.SPRITE.height, +y:-this.SPRITE_TOP_,"clip-path":"url(#blocklyTrashLidClipPath"+a+")"},this.svgGroup_);this.svgLid_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.workspace_.options.pathToMedia+Blockly.SPRITE.url);Blockly.bindEventWithChecks_(this.svgGroup_,"mouseup",this,this.click);Blockly.bindEvent_(c,"mouseover",this,this.mouseOver_);Blockly.bindEvent_(c,"mouseout",this,this.mouseOut_);this.animateLid_();return this.svgGroup_}; +Blockly.Trashcan.prototype.init=function(a){0this.minOpenness_&&1>this.lidOpen_&&(this.lidTask_=setTimeout(this.animateLid_.bind(this),20))}; +Blockly.Trashcan.prototype.setLidAngle_=function(a){var b=this.workspace_.toolboxPosition==Blockly.TOOLBOX_AT_RIGHT||this.workspace_.horizontalLayout&&this.workspace_.RTL;this.svgLid_.setAttribute("transform","rotate("+(b?-a:a)+","+(b?4:this.WIDTH_-4)+","+(this.LID_HEIGHT_-2)+")")};Blockly.Trashcan.prototype.close=function(){this.setOpen(!1)};Blockly.Trashcan.prototype.click=function(){if(this.contents_.length){for(var a=[],b=0,c;c=this.contents_[b];b++)a[b]=Blockly.Xml.textToDom(c);this.flyout.show(a)}}; +Blockly.Trashcan.prototype.mouseOver_=function(){this.contents_.length&&this.setOpen(!0)};Blockly.Trashcan.prototype.mouseOut_=function(){this.setOpen(!1)}; +Blockly.Trashcan.prototype.onDelete_=function(a){if(!(0>=this.workspace_.options.maxTrashcanContents)&&a.type==Blockly.Events.BLOCK_DELETE&&"shadow"!=a.oldXml.tagName.toLowerCase()&&(a=this.cleanBlockXML_(a.oldXml),-1==this.contents_.indexOf(a))){for(this.contents_.unshift(a);this.contents_.length>this.workspace_.options.maxTrashcanContents;)this.contents_.pop();this.minOpenness_=this.HAS_BLOCKS_LID_ANGLE;this.setLidAngle_(45*this.minOpenness_)}}; +Blockly.Trashcan.prototype.cleanBlockXML_=function(a){for(var b=a=a.cloneNode(!0);b;){b.removeAttribute&&(b.removeAttribute("x"),b.removeAttribute("y"),b.removeAttribute("id"),b.removeAttribute("disabled"),"comment"==b.nodeName&&(b.removeAttribute("h"),b.removeAttribute("w"),b.removeAttribute("pinned")));var c=b.firstChild||b.nextSibling;if(!c)for(c=b.parentNode;c;){if(c.nextSibling){c=c.nextSibling;break}c=c.parentNode}b=c}return Blockly.Xml.domToText(a)};Blockly.VariablesDynamic={};Blockly.VariablesDynamic.onCreateVariableButtonClick_String=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),void 0,"String")};Blockly.VariablesDynamic.onCreateVariableButtonClick_Number=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),void 0,"Number")};Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),void 0,"Colour")}; +Blockly.VariablesDynamic.flyoutCategory=function(a){var b=[],c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_STRING_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_STRING");b.push(c);c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_NUMBER_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_NUMBER");b.push(c);c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_COLOUR_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_COLOUR"); +b.push(c);a.registerButtonCallback("CREATE_VARIABLE_STRING",Blockly.VariablesDynamic.onCreateVariableButtonClick_String);a.registerButtonCallback("CREATE_VARIABLE_NUMBER",Blockly.VariablesDynamic.onCreateVariableButtonClick_Number);a.registerButtonCallback("CREATE_VARIABLE_COLOUR",Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour);a=Blockly.VariablesDynamic.flyoutCategoryBlocks(a);return b=b.concat(a)}; +Blockly.VariablesDynamic.flyoutCategoryBlocks=function(a){a=a.getAllVariables();var b=[];if(0image, .blocklyZoom>svg>image {","opacity: .4;","}",".blocklyZoom>image:hover, .blocklyZoom>svg>image:hover {","opacity: .6;","}",".blocklyZoom>image:active, .blocklyZoom>svg>image:active {","opacity: .8;","}"]);Blockly.Mutator=function(a){Blockly.Mutator.superClass_.constructor.call(this,null);this.quarkNames_=a};Blockly.utils.object.inherits(Blockly.Mutator,Blockly.Icon);Blockly.Mutator.prototype.workspaceWidth_=0;Blockly.Mutator.prototype.workspaceHeight_=0;Blockly.Mutator.prototype.setBlock=function(a){this.block_=a};Blockly.Mutator.prototype.getWorkspace=function(){return this.workspace_}; +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,toolboxPosition:this.block_.RTL?Blockly.TOOLBOX_AT_RIGHT:Blockly.TOOLBOX_AT_LEFT,horizontalLayout:!1,renderer:this.block_.workspace.options.renderer});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")))}; +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_(); +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.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.constants_.FULL_BLOCK_FIELDS&&(this.borderRect_?this.borderRect_.setAttribute("stroke",this.sourceBlock_.style.colourTertiary):this.sourceBlock_.pathObject.svgPath.setAttribute("fill",this.constants_.FIELD_BORDER_RECT_COLOUR))}; +Blockly.FieldTextInput.prototype.render_=function(){Blockly.FieldTextInput.superClass_.render_.call(this);if(this.isBeingEdited_){this.resizeEditor_();var a=this.htmlInput_;this.isTextValid_?(Blockly.utils.dom.removeClass(a,"blocklyInvalidInput"),Blockly.utils.aria.setState(a,Blockly.utils.aria.State.INVALID,!1)):(Blockly.utils.dom.addClass(a,"blocklyInvalidInput"),Blockly.utils.aria.setState(a,Blockly.utils.aria.State.INVALID,!0))}}; +Blockly.FieldTextInput.prototype.setSpellcheck=function(a){a!=this.spellcheck_&&(this.spellcheck_=a,this.htmlInput_&&this.htmlInput_.setAttribute("spellcheck",this.spellcheck_))};Blockly.FieldTextInput.prototype.showEditor_=function(a,b){this.workspace_=this.sourceBlock_.workspace;a=b||!1;!a&&(Blockly.utils.userAgent.MOBILE||Blockly.utils.userAgent.ANDROID||Blockly.utils.userAgent.IPAD)?this.showPromptEditor_():this.showInlineEditor_(a)}; +Blockly.FieldTextInput.prototype.showPromptEditor_=function(){var a=this;Blockly.prompt(Blockly.Msg.CHANGE_VALUE_TITLE,this.getText(),function(b){a.setValue(b)})};Blockly.FieldTextInput.prototype.showInlineEditor_=function(a){Blockly.WidgetDiv.show(this,this.sourceBlock_.RTL,this.widgetDispose_.bind(this));this.htmlInput_=this.widgetCreate_();this.isBeingEdited_=!0;a||(this.htmlInput_.focus({preventScroll:!0}),this.htmlInput_.select())}; +Blockly.FieldTextInput.prototype.widgetCreate_=function(){var a=Blockly.WidgetDiv.DIV;Blockly.utils.dom.addClass(this.getClickTarget_(),"editing");var b=document.createElement("input");b.className="blocklyHtmlInput";b.setAttribute("spellcheck",this.spellcheck_);var c=this.workspace_.scale,d=this.constants_.FIELD_TEXT_FONTSIZE*c+"pt";a.style.fontSize=d;b.style.fontSize=d;d=Blockly.FieldTextInput.BORDERRADIUS*c+"px";if(this.fullBlockClickTarget_){d=this.getScaledBBox();d=(d.bottom-d.top)/2+"px";var e= +this.sourceBlock_.getParent()?this.sourceBlock_.getParent().style.colourTertiary:this.sourceBlock_.style.colourTertiary;b.style.border=1*c+"px solid "+e;a.style.borderRadius=d;a.style.transition="box-shadow 0.25s ease 0s";this.constants_.FIELD_TEXTINPUT_BOX_SHADOW&&(a.style.boxShadow="rgba(255, 255, 255, 0.3) 0px 0px 0px "+4*c+"px")}b.style.borderRadius=d;a.appendChild(b);b.value=b.defaultValue=this.getEditorText_(this.value_);b.untypedDefaultValue_=this.value_;b.oldValue_=null;this.resizeEditor_(); +this.bindInputEvents_(b);return b};Blockly.FieldTextInput.prototype.widgetDispose_=function(){this.isBeingEdited_=!1;this.isTextValid_=!0;this.forceRerender();if(this.onFinishEditing_)this.onFinishEditing_(this.value_);this.unbindInputEvents_();var a=Blockly.WidgetDiv.DIV.style;a.width="auto";a.height="auto";a.fontSize="";a.transition="";a.boxShadow="";this.htmlInput_=null;Blockly.utils.dom.removeClass(this.getClickTarget_(),"editing")}; +Blockly.FieldTextInput.prototype.bindInputEvents_=function(a){this.onKeyDownWrapper_=Blockly.bindEventWithChecks_(a,"keydown",this,this.onHtmlInputKeyDown_);this.onKeyInputWrapper_=Blockly.bindEventWithChecks_(a,"input",this,this.onHtmlInputChange_);this.onBlurInputWrapper_=Blockly.bindEventWithChecks_(a,"blur",this,this.onHtmlInputBlur_)}; +Blockly.FieldTextInput.prototype.unbindInputEvents_=function(){this.onKeyDownWrapper_&&(Blockly.unbindEvent_(this.onKeyDownWrapper_),this.onKeyDownWrapper_=null);this.onKeyInputWrapper_&&(Blockly.unbindEvent_(this.onKeyInputWrapper_),this.onKeyInputWrapper_=null);this.onBlurInputWrapper_&&(Blockly.unbindEvent_(this.onBlurInputWrapper_),this.onBlurInputWrapper_=null)}; Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_=function(a){a.keyCode==Blockly.utils.KeyCodes.ENTER?(Blockly.WidgetDiv.hide(),Blockly.DropDownDiv.hideWithoutAnimation()):a.keyCode==Blockly.utils.KeyCodes.ESC?(this.htmlInput_.value=this.htmlInput_.defaultValue,Blockly.WidgetDiv.hide(),Blockly.DropDownDiv.hideWithoutAnimation()):a.keyCode==Blockly.utils.KeyCodes.TAB&&(Blockly.WidgetDiv.hide(),Blockly.DropDownDiv.hideWithoutAnimation(),this.sourceBlock_.tab(this,!a.shiftKey),a.preventDefault())}; -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(),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);b.y+=1;Blockly.utils.userAgent.GECKO&&Blockly.WidgetDiv.DIV.style.top&&(--b.x,--b.y);Blockly.utils.userAgent.WEBKIT&&(b.y-=3);a.style.left=b.x+"px";a.style.top=b.y+"px"}; +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.onHtmlInputBlur_=function(a){Blockly.WidgetDiv.hide();Blockly.DropDownDiv.hideWithoutAnimation()}; +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)};Blockly.utils.object.inherits(Blockly.FieldAngle,Blockly.FieldTextInput);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.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.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_()}; -Blockly.FieldAngle.prototype.showEditor_=function(){Blockly.FieldAngle.superClass_.showEditor_.call(this,Blockly.utils.userAgent.MOBILE||Blockly.utils.userAgent.ANDROID||Blockly.utils.userAgent.IPAD);var a=this.dropdownCreate_();Blockly.DropDownDiv.getContentDiv().appendChild(a);a=this.sourceBlock_.getColourBorder();a=a.colourBorder||a.colourLight;Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(),a);Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.updateGraph_()}; +Blockly.FieldAngle.prototype.showEditor_=function(a){Blockly.FieldAngle.superClass_.showEditor_.call(this,a,Blockly.utils.userAgent.MOBILE||Blockly.utils.userAgent.ANDROID||Blockly.utils.userAgent.IPAD);a=this.dropdownCreate_();Blockly.DropDownDiv.getContentDiv().appendChild(a);Blockly.DropDownDiv.setColour(this.sourceBlock_.style.colourPrimary,this.sourceBlock_.style.colourTertiary);Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.updateGraph_()}; Blockly.FieldAngle.prototype.dropdownCreate_=function(){var a=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",height:2*Blockly.FieldAngle.HALF+"px",width:2*Blockly.FieldAngle.HALF+"px",style:"touch-action: none"},null),b=Blockly.utils.dom.createSvgElement("circle",{cx:Blockly.FieldAngle.HALF,cy:Blockly.FieldAngle.HALF,r:Blockly.FieldAngle.RADIUS,"class":"blocklyAngleCircle"},a);this.gauge_= Blockly.utils.dom.createSvgElement("path",{"class":"blocklyAngleGauge"},a);this.line_=Blockly.utils.dom.createSvgElement("line",{x1:Blockly.FieldAngle.HALF,y1:Blockly.FieldAngle.HALF,"class":"blocklyAngleLine"},a);for(var c=0;360>c;c+=15)Blockly.utils.dom.createSvgElement("line",{x1:Blockly.FieldAngle.HALF+Blockly.FieldAngle.RADIUS,y1:Blockly.FieldAngle.HALF,x2:Blockly.FieldAngle.HALF+Blockly.FieldAngle.RADIUS-(0==c%45?10:5),y2:Blockly.FieldAngle.HALF,"class":"blocklyAngleMarks",transform:"rotate("+ -c+","+Blockly.FieldAngle.HALF+","+Blockly.FieldAngle.HALF+")"},a);this.clickWrapper_=Blockly.bindEventWithChecks_(a,"click",this,this.hide_);this.clickSurfaceWrapper_=Blockly.bindEventWithChecks_(b,"click",this,this.onMouseMove,!0,!0);this.moveSurfaceWrapper_=Blockly.bindEventWithChecks_(b,"mousemove",this,this.onMouseMove,!0,!0);return a};Blockly.FieldAngle.prototype.dropdownDispose_=function(){Blockly.unbindEvent_(this.clickWrapper_);Blockly.unbindEvent_(this.clickSurfaceWrapper_);Blockly.unbindEvent_(this.moveSurfaceWrapper_)}; -Blockly.FieldAngle.prototype.hide_=function(){Blockly.DropDownDiv.hideIfOwner(this);Blockly.WidgetDiv.hide()};Blockly.FieldAngle.prototype.onMouseMove=function(a){var b=this.gauge_.ownerSVGElement.getBoundingClientRect(),c=a.clientX-b.left-Blockly.FieldAngle.HALF;a=a.clientY-b.top-Blockly.FieldAngle.HALF;b=Math.atan(-a/c);isNaN(b)||(b=Blockly.utils.math.toDegrees(b),0>c?b+=180:0c?b+=180:0a&&(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);this.size_.width=Blockly.FieldCheckbox.WIDTH};Blockly.utils.object.inherits(Blockly.FieldCheckbox,Blockly.Field);Blockly.FieldCheckbox.fromJson=function(a){return new Blockly.FieldCheckbox(a.checked,void 0,a)};Blockly.FieldCheckbox.WIDTH=15;Blockly.FieldCheckbox.CHECK_CHAR="\u2713";Blockly.FieldCheckbox.CHECK_X_OFFSET=Blockly.Field.DEFAULT_TEXT_OFFSET-3; -Blockly.FieldCheckbox.CHECK_Y_OFFSET=14;Blockly.FieldCheckbox.prototype.SERIALIZABLE=!0;Blockly.FieldCheckbox.prototype.CURSOR="default";Blockly.FieldCheckbox.prototype.isDirty_=!1;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);this.textElement_.setAttribute("x",Blockly.FieldCheckbox.CHECK_X_OFFSET);this.textElement_.setAttribute("y",Blockly.FieldCheckbox.CHECK_Y_OFFSET);Blockly.utils.dom.addClass(this.textElement_,"blocklyCheckbox");this.textContent_.nodeValue=this.checkChar_||Blockly.FieldCheckbox.CHECK_CHAR;this.textElement_.style.display=this.value_?"block":"none"}; -Blockly.FieldCheckbox.prototype.setCheckCharacter=function(a){this.checkChar_=a;this.textContent_&&(this.textContent_.nodeValue=a||Blockly.FieldCheckbox.CHECK_CHAR)};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.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.FieldCheckbox.prototype.isDirty_=!1; +Blockly.FieldCheckbox.prototype.configure_=function(a){Blockly.FieldCheckbox.superClass_.configure_.call(this,a);a.checkCharacter&&(this.checkChar_=a.checkCharacter)}; +Blockly.FieldCheckbox.prototype.initView=function(){this.size_.width=this.constants_.FIELD_CHECKBOX_DEFAULT_WIDTH;Blockly.FieldCheckbox.superClass_.initView.call(this);this.textElement_.setAttribute("x",this.constants_.FIELD_CHECKBOX_X_OFFSET);this.textElement_.setAttribute("y",this.constants_.FIELD_CHECKBOX_Y_OFFSET);this.textElement_.removeAttribute("dominant-baseline");Blockly.utils.dom.addClass(this.textElement_,"blocklyCheckbox");this.textContent_.nodeValue=this.checkChar_||Blockly.FieldCheckbox.CHECK_CHAR; +this.textElement_.style.display=this.value_?"block":"none"};Blockly.FieldCheckbox.prototype.setCheckCharacter=function(a){this.checkChar_=a;this.textContent_&&(this.textContent_.nodeValue=a||Blockly.FieldCheckbox.CHECK_CHAR)};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.size_=new Blockly.utils.Size(Blockly.FieldColour.DEFAULT_WIDTH,Blockly.FieldColour.DEFAULT_HEIGHT)};Blockly.utils.object.inherits(Blockly.FieldColour,Blockly.Field);Blockly.FieldColour.fromJson=function(a){return new Blockly.FieldColour(a.colour,void 0,a)};Blockly.FieldColour.DEFAULT_WIDTH=26;Blockly.FieldColour.DEFAULT_HEIGHT=Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT; -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.createBorderRect_();this.borderRect_.style.fillOpacity=1;this.borderRect_.style.fill=this.value_};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)}; -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()}; +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.FieldColour.prototype.initView=function(){this.size_=new Blockly.utils.Size(this.constants_.FIELD_COLOUR_DEFAULT_WIDTH,this.constants_.FIELD_COLOUR_DEFAULT_HEIGHT);this.constants_.FIELD_COLOUR_FULL_BLOCK?this.clickTarget_=this.sourceBlock_.getSvgRoot():(this.createBorderRect_(),this.borderRect_.style.fillOpacity="1")}; +Blockly.FieldColour.prototype.applyColour=function(){this.constants_.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_.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.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)}; -Blockly.FieldColour.prototype.onMouseMove_=function(a){var b=(a=a.target)&&a.getAttribute("data-index");null!==b&&b!==this.highlightedIndex_&&this.setHighlightedCell_(a,Number(b))};Blockly.FieldColour.prototype.onMouseEnter_=function(){this.picker_.focus()};Blockly.FieldColour.prototype.onMouseLeave_=function(){this.picker_.blur();var a=this.getHighlighted_();a&&Blockly.utils.dom.removeClass(a,"blocklyColourHighlighted")}; +Blockly.FieldColour.prototype.onMouseMove_=function(a){var b=(a=a.target)&&Number(a.getAttribute("data-index"));null!==b&&b!==this.highlightedIndex_&&this.setHighlightedCell_(a,b)};Blockly.FieldColour.prototype.onMouseEnter_=function(){this.picker_.focus({preventScroll:!0})};Blockly.FieldColour.prototype.onMouseLeave_=function(){this.picker_.blur();var a=this.getHighlighted_();a&&Blockly.utils.dom.removeClass(a,"blocklyColourHighlighted")}; Blockly.FieldColour.prototype.getHighlighted_=function(){var a=this.columns_||Blockly.FieldColour.COLUMNS,b=this.picker_.childNodes[Math.floor(this.highlightedIndex_/a)];return b?b.childNodes[this.highlightedIndex_%a]:null}; Blockly.FieldColour.prototype.setHighlightedCell_=function(a,b){var c=this.getHighlighted_();c&&Blockly.utils.dom.removeClass(c,"blocklyColourHighlighted");Blockly.utils.dom.addClass(a,"blocklyColourHighlighted");this.highlightedIndex_=b;Blockly.utils.aria.setState(this.picker_,Blockly.utils.aria.State.ACTIVEDESCENDANT,a.getAttribute("id"))}; -Blockly.FieldColour.prototype.dropdownCreate_=function(){var a=this.columns_||Blockly.FieldColour.COLUMNS,b=this.colours_||Blockly.FieldColour.COLOURS,c=this.titles_||Blockly.FieldColour.TITLES,d=this.getValue(),e=document.createElement("table");e.className="blocklyColourTable";e.tabIndex=0;e.dir="ltr";Blockly.utils.aria.setRole(e,Blockly.utils.aria.Role.GRID);Blockly.utils.aria.setState(e,Blockly.utils.aria.State.EXPANDED,!0);Blockly.utils.aria.setState(e,"rowcount",Math.floor(b.length/a));Blockly.utils.aria.setState(e, -"colcount",a);for(var f,g=0;gtr>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.selectedIndex_=0;this.trimOptions_();a=this.getOptions(!1)[0];Blockly.FieldDropdown.superClass_.constructor.call(this,a[1],b,c);this.selectedMenuItem_=this.imageElement_=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(){Blockly.FieldDropdown.superClass_.initView.call(this);this.imageElement_=Blockly.utils.dom.createSvgElement("image",{y:Blockly.FieldDropdown.IMAGE_Y_OFFSET},this.fieldGroup_);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.showEditor_=function(){this.menu_=this.dropdownCreate_();this.menu_.render(Blockly.DropDownDiv.getContentDiv());Blockly.utils.dom.addClass(this.menu_.getElement(),"blocklyDropdownMenu");Blockly.DropDownDiv.showPositionedByField(this,this.dropdownDispose_.bind(this));this.menu_.focus();this.selectedMenuItem_&&Blockly.utils.style.scrollIntoContainerView(this.selectedMenuItem_.getElement(),this.menu_.getElement())}; -Blockly.FieldDropdown.prototype.dropdownCreate_=function(){var a=new Blockly.Menu;a.setRightToLeft(this.sourceBlock_.RTL);a.setRole("listbox");var b=this.getOptions(!1);this.selectedMenuItem_=null;for(var c=0;ca.length)){b=[];for(c=0;cthis.selectedIndex_)return null;var a=this.getOptions(!0)[this.selectedIndex_][0];return"object"==typeof a?a.alt:a}; +Blockly.FieldDropdown.prototype.doValueUpdate_=function(a){Blockly.FieldDropdown.superClass_.doValueUpdate_.call(this,a);a=this.getOptions(!0);for(var b=0,c;c=a[b];b++)c[1]==this.value_&&(this.selectedOption_=c)}; +Blockly.FieldDropdown.prototype.applyColour=function(){this.borderRect_&&(this.borderRect_.setAttribute("stroke",this.sourceBlock_.style.colourTertiary),this.menu_?this.borderRect_.setAttribute("fill",this.sourceBlock_.style.colourTertiary):this.borderRect_.setAttribute("fill","transparent"));this.sourceBlock_&&this.arrow_&&(this.sourceBlock_.isShadow()?this.arrow_.style.fill=this.sourceBlock_.style.colourSecondary:this.arrow_.style.fill=this.sourceBlock_.style.colourPrimary)}; +Blockly.FieldDropdown.prototype.render_=function(){this.textContent_.nodeValue="";this.imageElement_.style.display="none";var a=this.selectedOption_&&this.selectedOption_[0];a&&"object"==typeof a?this.renderSelectedImage_(a):this.renderSelectedText_();this.borderRect_&&(this.borderRect_.setAttribute("height",this.size_.height),this.borderRect_.setAttribute("width",this.size_.width))}; +Blockly.FieldDropdown.prototype.renderSelectedImage_=function(a){this.imageElement_.style.display="";this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",a.src);this.imageElement_.setAttribute("height",a.height);this.imageElement_.setAttribute("width",a.width);var b=Number(a.height),c=Number(a.width),d=!!this.borderRect_;this.size_.height=Math.max(d?this.constants_.FIELD_DROPDOWN_BORDER_RECT_HEIGHT:0,b+Blockly.FieldDropdown.IMAGE_Y_PADDING);a=this.size_.height/2;d=d?this.constants_.FIELD_BORDER_RECT_X_PADDING: +0;var e=this.svgArrow_?this.positionSVGArrow_(c+d,a-this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE/2):Blockly.utils.dom.getFastTextWidth(this.arrow_,this.constants_.FIELD_TEXT_FONTSIZE,this.constants_.FIELD_TEXT_FONTWEIGHT,this.constants_.FIELD_TEXT_FONTFAMILY);this.size_.width=c+e+2*d;this.sourceBlock_.RTL?(c=d-1,this.imageElement_.setAttribute("x",d+e),this.textElement_.setAttribute("x",c)):(c=c+e+d+1,this.textElement_.setAttribute("text-anchor","end"),this.textElement_.setAttribute("x",c),this.imageElement_.setAttribute("x", +d));this.imageElement_.setAttribute("y",a-b/2)}; +Blockly.FieldDropdown.prototype.renderSelectedText_=function(){this.textContent_.nodeValue=this.getDisplayText_();Blockly.utils.dom.addClass(this.textElement_,"blocklyDropdownText");this.textElement_.setAttribute("text-anchor","start");var a=!!this.borderRect_;this.size_.height=Math.max(a?this.constants_.FIELD_DROPDOWN_BORDER_RECT_HEIGHT:0,this.constants_.FIELD_TEXT_HEIGHT);var b=this.size_.height/2,c=Blockly.utils.dom.getFastTextWidth(this.textElement_,this.constants_.FIELD_TEXT_FONTSIZE,this.constants_.FIELD_TEXT_FONTWEIGHT, +this.constants_.FIELD_TEXT_FONTFAMILY);a=a?this.constants_.FIELD_BORDER_RECT_X_PADDING:0;var d=0;this.svgArrow_&&(d=this.positionSVGArrow_(c+a,b-this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE/2));this.size_.width=c+d+2*a;this.textElement_.setAttribute("x",this.sourceBlock_.RTL?this.size_.width-c-a:a);this.textElement_.setAttribute("y",b);this.constants_.FIELD_TEXT_BASELINE_CENTER||this.textElement_.setAttribute("dy",this.constants_.FIELD_TEXT_BASELINE_Y-this.constants_.FIELD_TEXT_HEIGHT/2+this.constants_.FIELD_TEXT_Y_OFFSET)}; +Blockly.FieldDropdown.prototype.positionSVGArrow_=function(a,b){if(!this.svgArrow_)return 0;var c=this.constants_.FIELD_DROPDOWN_SVG_ARROW_PADDING,d=this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE;this.svgArrow_.setAttribute("transform","translate("+(this.sourceBlock_.RTL?c:a+c)+","+b+")");return d+c};Blockly.FieldDropdown.prototype.getText_=function(){if(!this.selectedOption_)return null;var a=this.selectedOption_[0];return"object"==typeof a?a.alt:a}; Blockly.FieldDropdown.validateOptions_=function(a){if(!Array.isArray(a))throw TypeError("FieldDropdown options must be an array.");if(!a.length)throw TypeError("FieldDropdown options must not be an empty array.");for(var b=!1,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)};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.FieldDropdown.prototype.onBlocklyAction=function(a){if(this.menu_){if(a===Blockly.navigation.ACTION_PREVIOUS)return this.menu_.highlightPrevious(),!0;if(a===Blockly.navigation.ACTION_NEXT)return this.menu_.highlightNext(),!0}return Blockly.FieldDropdown.superClass_.onBlocklyAction.call(this,a)};Blockly.fieldRegistry.register("field_dropdown",Blockly.FieldDropdown);Blockly.FieldLabelSerializable=function(a,b,c){Blockly.FieldLabelSerializable.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldLabelSerializable,Blockly.FieldLabel);Blockly.FieldLabelSerializable.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldLabelSerializable(b,void 0,a)};Blockly.FieldLabelSerializable.prototype.EDITABLE=!1;Blockly.FieldLabelSerializable.prototype.SERIALIZABLE=!0; +Blockly.fieldRegistry.register("field_label_serializable",Blockly.FieldLabelSerializable);Blockly.FieldImage=function(a,b,c,d,e,f,g){if(!a)throw Error("Src value of an image field is required");a=Blockly.utils.replaceMessageReferences(a);c=Number(Blockly.utils.replaceMessageReferences(c));b=Number(Blockly.utils.replaceMessageReferences(b));if(isNaN(c)||isNaN(b))throw Error("Height and width values of an image field must cast to numbers.");if(0>=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)||""}; 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_)};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",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.FieldLabelSerializable=function(a,b,c){Blockly.FieldLabelSerializable.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldLabelSerializable,Blockly.FieldLabel);Blockly.FieldLabelSerializable.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldLabelSerializable(b,void 0,a)};Blockly.FieldLabelSerializable.prototype.EDITABLE=!1;Blockly.FieldLabelSerializable.prototype.SERIALIZABLE=!0; -Blockly.fieldRegistry.register("field_label_serializable",Blockly.FieldLabelSerializable);Blockly.FieldMultilineInput=function(a,b,c){null==a&&(a="");Blockly.FieldMultilineInput.superClass_.constructor.call(this,a,b,c)};Blockly.utils.object.inherits(Blockly.FieldMultilineInput,Blockly.FieldTextInput);Blockly.FieldMultilineInput.LINE_HEIGHT=20;Blockly.FieldMultilineInput.fromJson=function(a){var b=Blockly.utils.replaceMessageReferences(a.text);return new Blockly.FieldMultilineInput(b,void 0,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.LINE_HEIGHT=20;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=Blockly.Field.Y_PADDING/2,c=0,d=0;db&&(b=e);c+=Blockly.FieldMultilineInput.LINE_HEIGHT}this.borderRect_&&(b+=Blockly.Field.X_PADDING,this.borderRect_.setAttribute("width",b),this.borderRect_.setAttribute("height",c));this.size_.width=b;this.size_.height=c}; -Blockly.FieldMultilineInput.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.FieldMultilineInput.prototype.widgetCreate_=function(){var a=Blockly.WidgetDiv.DIV,b=this.workspace_.scale,c=document.createElement("textarea");c.className="blocklyHtmlInput blocklyHtmlTextAreaInput";c.setAttribute("spellcheck",this.spellcheck_);var d=Blockly.FieldTextInput.FONTSIZE*b+"pt";a.style.fontSize=d;c.style.fontSize=d;c.style.borderRadius=Blockly.FieldTextInput.BORDERRADIUS*b+"px";d=Blockly.Field.DEFAULT_TEXT_OFFSET*b;c.style.paddingLeft=d+"px";c.style.width="calc(100% - "+d+"px)"; -c.style.lineHeight=Blockly.FieldMultilineInput.LINE_HEIGHT*b+"px";a.appendChild(c);c.value=c.defaultValue=this.getEditorText_(this.value_);c.untypedDefaultValue_=this.value_;c.oldValue_=null;Blockly.utils.userAgent.GECKO?setTimeout(this.resizeEditor_.bind(this),0):this.resizeEditor_();this.bindInputEvents_(c);return c}; +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;cb&&(b=e);c+=Blockly.FieldMultilineInput.LINE_HEIGHT}this.borderRect_&&(b+=2*this.constants_.FIELD_BORDER_RECT_X_PADDING,this.borderRect_.setAttribute("width",b),this.borderRect_.setAttribute("height",c));this.size_.width=b;this.size_.height=c}; +Blockly.FieldMultilineInput.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.FieldMultilineInput.prototype.widgetCreate_=function(){var a=Blockly.WidgetDiv.DIV,b=this.workspace_.scale,c=document.createElement("textarea");c.className="blocklyHtmlInput blocklyHtmlTextAreaInput";c.setAttribute("spellcheck",this.spellcheck_);var d=this.constants_.FIELD_TEXT_FONTSIZE*b+"pt";a.style.fontSize=d;c.style.fontSize=d;c.style.borderRadius=Blockly.FieldTextInput.BORDERRADIUS*b+"px";d=this.constants_.FIELD_BORDER_RECT_X_PADDING*b;c.style.paddingLeft=d+"px";c.style.width="calc(100% - "+ +d+"px)";c.style.lineHeight=Blockly.FieldMultilineInput.LINE_HEIGHT*b+"px";a.appendChild(c);c.value=c.defaultValue=this.getEditorText_(this.value_);c.untypedDefaultValue_=this.value_;c.oldValue_=null;Blockly.utils.userAgent.GECKO?setTimeout(this.resizeEditor_.bind(this),0):this.resizeEditor_();this.bindInputEvents_(c);return c}; Blockly.FieldMultilineInput.prototype.onHtmlInputKeyDown_=function(a){a.keyCode!==Blockly.utils.KeyCodes.ENTER&&Blockly.FieldMultilineInput.superClass_.onHtmlInputKeyDown_.call(this,a)};Blockly.Css.register(".blocklyHtmlTextAreaInput {,font-family: monospace;,resize: none;,overflow: hidden;,height: 100%;,text-align: left;,}".split(","));Blockly.fieldRegistry.register("field_multilinetext",Blockly.FieldMultilineInput);Blockly.FieldNumber=function(a,b,c,d,e,f){this.min_=-Infinity;this.max_=Infinity;this.precision_=0;this.decimalPlaces_=null;Blockly.FieldNumber.superClass_.constructor.call(this,a||0,e,f);f||this.setConstraints(b,c,d)};Blockly.utils.object.inherits(Blockly.FieldNumber,Blockly.FieldTextInput);Blockly.FieldNumber.fromJson=function(a){return new Blockly.FieldNumber(a.value,void 0,void 0,void 0,void 0,a)};Blockly.FieldNumber.prototype.SERIALIZABLE=!0; Blockly.FieldNumber.prototype.configure_=function(a){Blockly.FieldNumber.superClass_.configure_.call(this,a);this.setMinInternal_(a.min);this.setMaxInternal_(a.max);this.setPrecisionInternal_(a.precision)};Blockly.FieldNumber.prototype.setConstraints=function(a,b,c){this.setMinInternal_(a);this.setMaxInternal_(b);this.setPrecisionInternal_(c);this.setValue(this.getValue())};Blockly.FieldNumber.prototype.setMin=function(a){this.setMinInternal_(a);this.setValue(this.getValue())}; Blockly.FieldNumber.prototype.setMinInternal_=function(a){null==a?this.min_=-Infinity:(a=Number(a),isNaN(a)||(this.min_=a))};Blockly.FieldNumber.prototype.getMin=function(){return this.min_};Blockly.FieldNumber.prototype.setMax=function(a){this.setMaxInternal_(a);this.setValue(this.getValue())};Blockly.FieldNumber.prototype.setMaxInternal_=function(a){null==a?this.max_=Infinity:(a=Number(a),isNaN(a)||(this.max_=a))};Blockly.FieldNumber.prototype.getMax=function(){return this.max_}; Blockly.FieldNumber.prototype.setPrecision=function(a){this.setPrecisionInternal_(a);this.setValue(this.getValue())};Blockly.FieldNumber.prototype.setPrecisionInternal_=function(a){null==a?this.precision_=0:(a=Number(a),isNaN(a)||(this.precision_=a));var b=this.precision_.toString(),c=b.indexOf(".");this.decimalPlaces_=-1==c?a?0:null:b.length-c-1};Blockly.FieldNumber.prototype.getPrecision=function(){return this.precision_}; -Blockly.FieldNumber.prototype.doClassValidation_=function(a){if(null===a)return null;a=String(a);a=a.replace(/O/ig,"0");a=a.replace(/,/g,"");a=Number(a||0);if(isNaN(a))return null;a=Math.min(Math.max(a,this.min_),this.max_);this.precision_&&isFinite(a)&&(a=Math.round(a/this.precision_)*this.precision_);null!=this.decimalPlaces_&&(a=Number(a.toFixed(this.decimalPlaces_)));return a}; -Blockly.FieldNumber.prototype.widgetCreate_=function(){var a=Blockly.FieldNumber.superClass_.widgetCreate_.call(this);-Infinitythis.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,Blockly.BlockSvg.MIN_BLOCK_Y);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_);Blockly.Events.disable();this.setValue(a.getId());Blockly.Events.enable()}}; +Blockly.FieldNumber.prototype.doClassValidation_=function(a){if(null===a)return null;a=String(a);a=a.replace(/O/ig,"0");a=a.replace(/,/g,"");a=a.replace(/infinity/i,"Infinity");a=Number(a||0);if(isNaN(a))return null;a=Math.min(Math.max(a,this.min_),this.max_);this.precision_&&isFinite(a)&&(a=Math.round(a/this.precision_)*this.precision_);null!=this.decimalPlaces_&&(a=Number(a.toFixed(this.decimalPlaces_)));return a}; +Blockly.FieldNumber.prototype.widgetCreate_=function(){var a=Blockly.FieldNumber.superClass_.widgetCreate_.call(this);-Infinitythis.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.shouldAddBorderRect_=function(){return Blockly.FieldVariable.superClass_.shouldAddBorderRect_.call(this)&&(!this.constants_.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)}; Blockly.FieldVariable.prototype.getValue=function(){return this.variable_?this.variable_.getId():null};Blockly.FieldVariable.prototype.getText=function(){return this.variable_?this.variable_.name:""};Blockly.FieldVariable.prototype.getVariable=function(){return this.variable_};Blockly.FieldVariable.prototype.getValidator=function(){return this.variable_?this.validator_:null}; Blockly.FieldVariable.prototype.doClassValidation_=function(a){if(null===a)return null;var b=Blockly.Variables.getVariable(this.sourceBlock_.workspace,a);if(!b)return console.warn("Variable id doesn't point to a real variable! ID was "+a),null;b=b.type;return this.typeIsAllowed_(b)?a:(console.warn("Variable type doesn't match this field! Type was "+b),null)}; Blockly.FieldVariable.prototype.doValueUpdate_=function(a){this.variable_=Blockly.Variables.getVariable(this.sourceBlock_.workspace,a);Blockly.FieldVariable.superClass_.doValueUpdate_.call(this,a)};Blockly.FieldVariable.prototype.typeIsAllowed_=function(a){var b=this.getVariableTypes_();if(!b)return!0;for(var c=0;c90-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.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,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;if(this.toolboxPosition_==Blockly.TOOLBOX_AT_LEFT)return new Blockly.utils.Rect(-1E9,1E9,-1E9,b+a.width);Blockly.utils.userAgent.GECKO&&this.targetWorkspace_&&this.targetWorkspace_.isMutator&&(a=this.targetWorkspace_.svgGroup_.getBoundingClientRect().x,10>Math.abs(a-b)&&(b+=this.leftEdge_*this.targetWorkspace_.options.parentWorkspace.scale));return 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.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||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.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_.svgPath_,this.iconXY_,null,null);this.bubble_.setSvgId(this.block_.id);if(a=this.workspace_.options.languageTree)this.workspace_.flyout_.init(this.workspace_),this.workspace_.flyout_.show(a.childNodes);this.rootBlock_=this.block_.decompose(this.workspace_);a=this.rootBlock_.getDescendants(!1); -for(var b=0,c;c=a[b];b++)c.render();this.rootBlock_.setMovable(!1);this.rootBlock_.setDeletable(!1);this.workspace_.flyout_?(a=2*this.workspace_.flyout_.CORNER_RADIUS,b=this.workspace_.getFlyout().getWidth()+a):b=a=16;this.block_.RTL&&(b=-b);this.rootBlock_.moveBy(b,a);if(this.block_.saveConnections){var d=this;this.block_.saveConnections(this.rootBlock_);this.sourceListener_=function(){d.block_.saveConnections(d.rootBlock_)};this.block_.workspace.addChangeListener(this.sourceListener_)}this.resizeBubble_(); -this.workspace_.addChangeListener(this.workspaceChanged_.bind(this));this.updateColour()}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);b=c.rendered;c.rendered=!1; -c.compose(this.rootBlock_);c.rendered=b;c.initSvg();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)}c.rendered&&c.render();a!=b&&Blockly.keyboardAccessibilityMode&&Blockly.navigation.moveCursorOnBlockMutation(c);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.updateBlockStyle=function(){var a=this.workspace_;if(a&&a.getAllBlocks()){for(var b=a.getAllBlocks(),c=0;crect,",a+" .blocklyEditableText>rect {","fill: "+this.FIELD_BORDER_RECT_COLOUR+";","fill-opacity: .6;","stroke: none;","}",a+" .blocklyNonEditableText>text,",a+" .blocklyEditableText>text {", +"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;","}"]};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_();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.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_()}; +Blockly.blockRendering.MarkerSvg.prototype.showCurrent_=function(){this.hide();this.currentMarkerSvg.style.display=""};Blockly.blockRendering.MarkerSvg.prototype.positionBlock_=function(a,b,c){a=Blockly.utils.svgPaths.moveBy(-b,c)+Blockly.utils.svgPaths.lineOnAxis("V",-b)+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.positionInput_=function(a){var b=a.getOffsetInBlock().x,c=a.getOffsetInBlock().y;a=Blockly.utils.svgPaths.moveTo(0,0)+this.constants_.shapeFor(a).pathDown;this.markerInput_.setAttribute("d",a);this.markerInput_.setAttribute("transform","translate("+b+","+c+")"+(this.workspace_.RTL?" scale(-1 1)":""));this.currentMarkerSvg=this.markerInput_}; +Blockly.blockRendering.MarkerSvg.prototype.positionLine_=function(a,b,c){this.markerSvgLine_.setAttribute("x",a);this.markerSvgLine_.setAttribute("y",b);this.markerSvgLine_.setAttribute("width",c);this.currentMarkerSvg=this.markerSvgLine_}; +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){b?(this.showAtLocation_(b),this.firemarkerEvent_(a,b),a=this.currentMarkerSvg.childNodes[0],void 0!==a&&a.beginElement&&a.beginElement()):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.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",{fill:this.colour_,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",stroke:this.colour_},this.markerSvg_);this.markerInput_=Blockly.utils.dom.createSvgElement("path",{transform:"",style:"display: none",fill:this.colour_},this.markerSvg_);this.markerBlock_=Blockly.utils.dom.createSvgElement("path",{transform:"",style:"display: none",fill:"none",stroke:this.colour_,"stroke-width":this.constants_.CURSOR_STROKE_WIDTH},this.markerSvg_);if(this.isCursor()){var a=this.getBlinkProperties_();Blockly.utils.dom.createSvgElement("animate",this.getBlinkProperties_(), +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.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}; @@ -1048,71 +1129,86 @@ Blockly.blockRendering.Types.isInlineInput=function(a){return a.type&Blockly.blo Blockly.blockRendering.Types.isPreviousOrNextConnection=function(a){return a.type&(Blockly.blockRendering.Types.PREVIOUS_CONNECTION|Blockly.blockRendering.Types.NEXT_CONNECTION)};Blockly.blockRendering.Types.isLeftRoundedCorner=function(a){return a.type&Blockly.blockRendering.Types.LEFT_ROUND_CORNER};Blockly.blockRendering.Types.isRightRoundedCorner=function(a){return a.type&Blockly.blockRendering.Types.RIGHT_ROUND_CORNER}; Blockly.blockRendering.Types.isLeftSquareCorner=function(a){return a.type&Blockly.blockRendering.Types.LEFT_SQUARE_CORNER};Blockly.blockRendering.Types.isRightSquareCorner=function(a){return a.type&Blockly.blockRendering.Types.RIGHT_SQUARE_CORNER};Blockly.blockRendering.Types.isCorner=function(a){return a.type&Blockly.blockRendering.Types.CORNER};Blockly.blockRendering.Types.isJaggedEdge=function(a){return a.type&Blockly.blockRendering.Types.JAGGED_EDGE}; Blockly.blockRendering.Types.isRow=function(a){return a.type&Blockly.blockRendering.Types.ROW};Blockly.blockRendering.Types.isBetweenRowSpacer=function(a){return a.type&Blockly.blockRendering.Types.BETWEEN_ROW_SPACER};Blockly.blockRendering.Types.isTopRow=function(a){return a.type&Blockly.blockRendering.Types.TOP_ROW};Blockly.blockRendering.Types.isBottomRow=function(a){return a.type&Blockly.blockRendering.Types.BOTTOM_ROW}; -Blockly.blockRendering.Types.isTopOrBottomRow=function(a){return a.type&(Blockly.blockRendering.Types.TOP_ROW|Blockly.blockRendering.Types.BOTTOM_ROW)};Blockly.blockRendering.Types.isInputRow=function(a){return a.type&Blockly.blockRendering.Types.INPUT_ROW};Blockly.blockRendering.Measurable=function(a){this.height=this.width=0;this.type=Blockly.blockRendering.Types.NONE;this.centerline=this.xPos=0;this.constants_=a;this.notchOffset=this.constants_.NOTCH_OFFSET_LEFT};Blockly.blockRendering.Connection=function(a,b){Blockly.blockRendering.Connection.superClass_.constructor.call(this,a);this.connectionModel=b;this.shape=this.constants_.shapeFor(b);this.type|=Blockly.blockRendering.Types.CONNECTION};Blockly.utils.object.inherits(Blockly.blockRendering.Connection,Blockly.blockRendering.Measurable); -Blockly.blockRendering.OutputConnection=function(a,b){Blockly.blockRendering.OutputConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.OUTPUT_CONNECTION;this.height=this.shape.height;this.width=this.shape.width;this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.startX=this.width};Blockly.utils.object.inherits(Blockly.blockRendering.OutputConnection,Blockly.blockRendering.Connection);Blockly.blockRendering.OutputConnection.prototype.isDynamic=function(){return this.shape.isDynamic}; +Blockly.blockRendering.Types.isTopOrBottomRow=function(a){return a.type&(Blockly.blockRendering.Types.TOP_ROW|Blockly.blockRendering.Types.BOTTOM_ROW)};Blockly.blockRendering.Types.isInputRow=function(a){return a.type&Blockly.blockRendering.Types.INPUT_ROW};Blockly.blockRendering.Measurable=function(a){this.height=this.width=0;this.type=Blockly.blockRendering.Types.NONE;this.centerline=this.xPos=0;this.constants_=a;this.notchOffset=this.constants_.NOTCH_OFFSET_LEFT};Blockly.blockRendering.Connection=function(a,b){Blockly.blockRendering.Connection.superClass_.constructor.call(this,a);this.connectionModel=b;this.shape=this.constants_.shapeFor(b);this.isDynamicShape=!!this.shape.isDynamic;this.type|=Blockly.blockRendering.Types.CONNECTION};Blockly.utils.object.inherits(Blockly.blockRendering.Connection,Blockly.blockRendering.Measurable); +Blockly.blockRendering.OutputConnection=function(a,b){Blockly.blockRendering.OutputConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.OUTPUT_CONNECTION;this.height=this.isDynamicShape?0:this.shape.height;this.startX=this.width=this.isDynamicShape?0:this.shape.width;this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.connectionOffsetX=0};Blockly.utils.object.inherits(Blockly.blockRendering.OutputConnection,Blockly.blockRendering.Connection); Blockly.blockRendering.PreviousConnection=function(a,b){Blockly.blockRendering.PreviousConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.PREVIOUS_CONNECTION;this.height=this.shape.height;this.width=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.PreviousConnection,Blockly.blockRendering.Connection); -Blockly.blockRendering.NextConnection=function(a,b){Blockly.blockRendering.NextConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.NEXT_CONNECTION;this.height=this.shape.height;this.width=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.NextConnection,Blockly.blockRendering.Connection);Blockly.blockRendering.InputConnection=function(a,b){Blockly.blockRendering.InputConnection.superClass_.constructor.call(this,a,b.connection);this.type|=Blockly.blockRendering.Types.INPUT;this.input=b;this.align=b.align;if(this.connectedBlock=b.connection&&b.connection.targetBlock()?b.connection.targetBlock():null){var c=this.connectedBlock.getHeightWidth();this.connectedBlockWidth=c.width;this.connectedBlockHeight=c.height}else this.connectedBlockHeight=this.connectedBlockWidth=0;this.connection= -b.connection;this.connectionOffsetY=this.connectionOffsetX=0};Blockly.utils.object.inherits(Blockly.blockRendering.InputConnection,Blockly.blockRendering.Connection); -Blockly.blockRendering.InlineInput=function(a,b){Blockly.blockRendering.InlineInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.INLINE_INPUT;this.connectedBlock?(this.width=this.connectedBlockWidth,this.height=this.connectedBlockHeight):(this.height=this.constants_.EMPTY_INLINE_INPUT_HEIGHT,this.width=this.shape.width+this.constants_.EMPTY_INLINE_INPUT_PADDING);this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.connectionHeight=this.shape.height;this.connectionWidth= -this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.InlineInput,Blockly.blockRendering.InputConnection);Blockly.blockRendering.StatementInput=function(a,b){Blockly.blockRendering.StatementInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.STATEMENT_INPUT;this.height=this.connectedBlock?this.connectedBlockHeight+this.constants_.STATEMENT_BOTTOM_SPACER:this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT;this.width=this.constants_.NOTCH_OFFSET_LEFT+this.shape.width}; -Blockly.utils.object.inherits(Blockly.blockRendering.StatementInput,Blockly.blockRendering.InputConnection); -Blockly.blockRendering.ExternalValueInput=function(a,b){Blockly.blockRendering.ExternalValueInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.EXTERNAL_VALUE_INPUT;this.height=this.connectedBlock?this.connectedBlockHeight-2*this.constants_.TAB_OFFSET_FROM_TOP:this.shape.height;this.width=this.shape.width+this.constants_.EXTERNAL_VALUE_INPUT_PADDING;this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.connectionHeight=this.shape.height;this.connectionWidth= -this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.ExternalValueInput,Blockly.blockRendering.InputConnection);Blockly.blockRendering.Icon=function(a,b){Blockly.blockRendering.Icon.superClass_.constructor.call(this,a);this.icon=b;this.isVisible=b.isVisible();this.type|=Blockly.blockRendering.Types.ICON;var c=b.getCorrectedSize();this.height=c.height;this.width=c.width};Blockly.utils.object.inherits(Blockly.blockRendering.Icon,Blockly.blockRendering.Measurable); +Blockly.blockRendering.NextConnection=function(a,b){Blockly.blockRendering.NextConnection.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.NEXT_CONNECTION;this.height=this.shape.height;this.width=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.NextConnection,Blockly.blockRendering.Connection);Blockly.blockRendering.InputConnection=function(a,b){Blockly.blockRendering.InputConnection.superClass_.constructor.call(this,a,b.connection);this.type|=Blockly.blockRendering.Types.INPUT;this.input=b;this.align=b.align;(this.connectedBlock=b.connection&&b.connection.targetBlock()?b.connection.targetBlock():null)?(a=this.connectedBlock.getHeightWidth(),this.connectedBlockWidth=a.width,this.connectedBlockHeight=a.height):this.connectedBlockHeight=this.connectedBlockWidth=0;this.connectionOffsetY=this.connectionOffsetX= +0};Blockly.utils.object.inherits(Blockly.blockRendering.InputConnection,Blockly.blockRendering.Connection); +Blockly.blockRendering.InlineInput=function(a,b){Blockly.blockRendering.InlineInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.INLINE_INPUT;this.connectedBlock?(this.width=this.connectedBlockWidth,this.height=this.connectedBlockHeight):(this.height=this.constants_.EMPTY_INLINE_INPUT_HEIGHT,this.width=this.constants_.EMPTY_INLINE_INPUT_PADDING);this.connectionHeight=this.isDynamicShape?this.shape.height(this.height):this.shape.height;this.connectionWidth=this.isDynamicShape? +this.shape.width(this.height):this.shape.width;this.connectedBlock||(this.width+=this.connectionWidth*(this.isDynamicShape?2:1));this.connectionOffsetY=this.isDynamicShape?this.shape.connectionOffsetY(this.connectionHeight):this.constants_.TAB_OFFSET_FROM_TOP;this.connectionOffsetX=this.isDynamicShape?this.shape.connectionOffsetX(this.connectionWidth):0};Blockly.utils.object.inherits(Blockly.blockRendering.InlineInput,Blockly.blockRendering.InputConnection); +Blockly.blockRendering.StatementInput=function(a,b){Blockly.blockRendering.StatementInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.STATEMENT_INPUT;this.height=this.connectedBlock?this.connectedBlockHeight+this.constants_.STATEMENT_BOTTOM_SPACER:this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT;this.width=this.constants_.STATEMENT_INPUT_NOTCH_OFFSET+this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.StatementInput,Blockly.blockRendering.InputConnection); +Blockly.blockRendering.ExternalValueInput=function(a,b){Blockly.blockRendering.ExternalValueInput.superClass_.constructor.call(this,a,b);this.type|=Blockly.blockRendering.Types.EXTERNAL_VALUE_INPUT;this.height=this.connectedBlock?this.connectedBlockHeight-this.constants_.TAB_OFFSET_FROM_TOP-this.constants_.MEDIUM_PADDING:this.shape.height;this.width=this.shape.width+this.constants_.EXTERNAL_VALUE_INPUT_PADDING;this.connectionOffsetY=this.constants_.TAB_OFFSET_FROM_TOP;this.connectionHeight=this.shape.height; +this.connectionWidth=this.shape.width};Blockly.utils.object.inherits(Blockly.blockRendering.ExternalValueInput,Blockly.blockRendering.InputConnection);Blockly.blockRendering.Icon=function(a,b){Blockly.blockRendering.Icon.superClass_.constructor.call(this,a);this.icon=b;this.isVisible=b.isVisible();this.type|=Blockly.blockRendering.Types.ICON;a=b.getCorrectedSize();this.height=a.height;this.width=a.width};Blockly.utils.object.inherits(Blockly.blockRendering.Icon,Blockly.blockRendering.Measurable); Blockly.blockRendering.JaggedEdge=function(a){Blockly.blockRendering.JaggedEdge.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.JAGGED_EDGE;this.height=this.constants_.JAGGED_TEETH.height;this.width=this.constants_.JAGGED_TEETH.width};Blockly.utils.object.inherits(Blockly.blockRendering.JaggedEdge,Blockly.blockRendering.Measurable); Blockly.blockRendering.Field=function(a,b,c){Blockly.blockRendering.Field.superClass_.constructor.call(this,a);this.field=b;this.isEditable=b.isCurrentlyEditable();this.flipRtl=b.getFlipRtl();this.type|=Blockly.blockRendering.Types.FIELD;a=this.field.getSize();this.height=a.height;this.width=a.width;this.parentInput=c};Blockly.utils.object.inherits(Blockly.blockRendering.Field,Blockly.blockRendering.Measurable); Blockly.blockRendering.Hat=function(a){Blockly.blockRendering.Hat.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.HAT;this.height=this.constants_.START_HAT.height;this.width=this.constants_.START_HAT.width;this.ascenderHeight=this.height};Blockly.utils.object.inherits(Blockly.blockRendering.Hat,Blockly.blockRendering.Measurable); Blockly.blockRendering.SquareCorner=function(a,b){Blockly.blockRendering.SquareCorner.superClass_.constructor.call(this,a);this.type=(b&&"left"!=b?Blockly.blockRendering.Types.RIGHT_SQUARE_CORNER:Blockly.blockRendering.Types.LEFT_SQUARE_CORNER)|Blockly.blockRendering.Types.CORNER;this.width=this.height=this.constants_.NO_PADDING};Blockly.utils.object.inherits(Blockly.blockRendering.SquareCorner,Blockly.blockRendering.Measurable); Blockly.blockRendering.RoundCorner=function(a,b){Blockly.blockRendering.RoundCorner.superClass_.constructor.call(this,a);this.type=(b&&"left"!=b?Blockly.blockRendering.Types.RIGHT_ROUND_CORNER:Blockly.blockRendering.Types.LEFT_ROUND_CORNER)|Blockly.blockRendering.Types.CORNER;this.width=this.constants_.CORNER_RADIUS;this.height=this.constants_.CORNER_RADIUS/2};Blockly.utils.object.inherits(Blockly.blockRendering.RoundCorner,Blockly.blockRendering.Measurable); -Blockly.blockRendering.InRowSpacer=function(a,b){Blockly.blockRendering.InRowSpacer.superClass_.constructor.call(this,a);this.type=this.type|Blockly.blockRendering.Types.SPACER|Blockly.blockRendering.Types.IN_ROW_SPACER;this.width=b;this.height=this.constants_.SPACER_DEFAULT_HEIGHT};Blockly.utils.object.inherits(Blockly.blockRendering.InRowSpacer,Blockly.blockRendering.Measurable);Blockly.blockRendering.Row=function(a){this.type=Blockly.blockRendering.Types.ROW;this.elements=[];this.xPos=this.yPos=this.widthWithConnectedBlocks=this.minWidth=this.minHeight=this.width=this.height=0;this.hasJaggedEdge=this.hasDummyInput=this.hasInlineInput=this.hasStatement=this.hasExternalInput=!1;this.constants_=a;this.notchOffset=this.constants_.NOTCH_OFFSET_LEFT};Blockly.blockRendering.Row.prototype.measure=function(){throw Error("Unexpected attempt to measure a base Row.");}; -Blockly.blockRendering.Row.prototype.getLastInput=function(){for(var a=this.elements.length-1,b;b=this.elements[a];a--)if(Blockly.blockRendering.Types.isInput(b))return b;return null};Blockly.blockRendering.Row.prototype.startsWithElemSpacer=function(){return!0};Blockly.blockRendering.Row.prototype.endsWithElemSpacer=function(){return!0};Blockly.blockRendering.Row.prototype.getFirstSpacer=function(){for(var a=0,b;b=this.elements[a];a++)if(Blockly.blockRendering.Types.isSpacer(b))return b;return null}; -Blockly.blockRendering.Row.prototype.getLastSpacer=function(){for(var a=this.elements.length-1,b;b=this.elements[a];a--)if(Blockly.blockRendering.Types.isSpacer(b))return b;return null};Blockly.blockRendering.TopRow=function(a){Blockly.blockRendering.TopRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.TOP_ROW;this.ascenderHeight=this.capline=0;this.hasPreviousConnection=!1;this.connection=null};Blockly.utils.object.inherits(Blockly.blockRendering.TopRow,Blockly.blockRendering.Row); -Blockly.blockRendering.TopRow.prototype.hasLeftSquareCorner=function(a){var b=(a.hat?"cap"===a.hat:Blockly.BlockSvg.START_HAT)&&!a.outputConnection&&!a.previousConnection,c=a.getPreviousBlock();return!!a.outputConnection||b||(c?c.getNextBlock()==a:!1)}; +Blockly.blockRendering.InRowSpacer=function(a,b){Blockly.blockRendering.InRowSpacer.superClass_.constructor.call(this,a);this.type=this.type|Blockly.blockRendering.Types.SPACER|Blockly.blockRendering.Types.IN_ROW_SPACER;this.width=b;this.height=this.constants_.SPACER_DEFAULT_HEIGHT};Blockly.utils.object.inherits(Blockly.blockRendering.InRowSpacer,Blockly.blockRendering.Measurable);Blockly.blockRendering.Row=function(a){this.type=Blockly.blockRendering.Types.ROW;this.elements=[];this.xPos=this.yPos=this.widthWithConnectedBlocks=this.minWidth=this.minHeight=this.width=this.height=0;this.hasJaggedEdge=this.hasDummyInput=this.hasInlineInput=this.hasStatement=this.hasExternalInput=!1;this.constants_=a;this.notchOffset=this.constants_.NOTCH_OFFSET_LEFT;this.align=null}; +Blockly.blockRendering.Row.prototype.measure=function(){throw Error("Unexpected attempt to measure a base Row.");};Blockly.blockRendering.Row.prototype.getLastInput=function(){for(var a=this.elements.length-1,b;b=this.elements[a];a--)if(Blockly.blockRendering.Types.isInput(b))return b;return null};Blockly.blockRendering.Row.prototype.startsWithElemSpacer=function(){return!0};Blockly.blockRendering.Row.prototype.endsWithElemSpacer=function(){return!0}; +Blockly.blockRendering.Row.prototype.getFirstSpacer=function(){for(var a=0,b;b=this.elements[a];a++)if(Blockly.blockRendering.Types.isSpacer(b))return b;return null};Blockly.blockRendering.Row.prototype.getLastSpacer=function(){for(var a=this.elements.length-1,b;b=this.elements[a];a--)if(Blockly.blockRendering.Types.isSpacer(b))return b;return null}; +Blockly.blockRendering.TopRow=function(a){Blockly.blockRendering.TopRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.TOP_ROW;this.ascenderHeight=this.capline=0;this.hasPreviousConnection=!1;this.connection=null};Blockly.utils.object.inherits(Blockly.blockRendering.TopRow,Blockly.blockRendering.Row); +Blockly.blockRendering.TopRow.prototype.hasLeftSquareCorner=function(a){var b=("undefined"!==typeof a.hat?"cap"===a.hat:this.constants_.ADD_START_HATS)&&!a.outputConnection&&!a.previousConnection,c=a.getPreviousBlock();return!!a.outputConnection||b||(c?c.getNextBlock()==a:!1)};Blockly.blockRendering.TopRow.prototype.hasRightSquareCorner=function(a){return!0}; Blockly.blockRendering.TopRow.prototype.measure=function(){for(var a=0,b=0,c=0,d=0,e;e=this.elements[d];d++)b+=e.width,Blockly.blockRendering.Types.isSpacer(e)||(Blockly.blockRendering.Types.isHat(e)?c=Math.max(c,e.ascenderHeight):a=Math.max(a,e.height));this.width=Math.max(this.minWidth,b);this.height=Math.max(this.minHeight,a)+c;this.capline=this.ascenderHeight=c;this.widthWithConnectedBlocks=this.width};Blockly.blockRendering.TopRow.prototype.startsWithElemSpacer=function(){return!1}; -Blockly.blockRendering.BottomRow=function(a){Blockly.blockRendering.BottomRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.BOTTOM_ROW;this.hasNextConnection=!1;this.connection=null;this.baseline=this.descenderHeight=0};Blockly.utils.object.inherits(Blockly.blockRendering.BottomRow,Blockly.blockRendering.Row);Blockly.blockRendering.BottomRow.prototype.hasLeftSquareCorner=function(a){return!!a.outputConnection||!!a.getNextBlock()}; +Blockly.blockRendering.TopRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.blockRendering.BottomRow=function(a){Blockly.blockRendering.BottomRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.BOTTOM_ROW;this.hasNextConnection=!1;this.connection=null;this.baseline=this.descenderHeight=0};Blockly.utils.object.inherits(Blockly.blockRendering.BottomRow,Blockly.blockRendering.Row); +Blockly.blockRendering.BottomRow.prototype.hasLeftSquareCorner=function(a){return!!a.outputConnection||!!a.getNextBlock()};Blockly.blockRendering.BottomRow.prototype.hasRightSquareCorner=function(a){return!0}; Blockly.blockRendering.BottomRow.prototype.measure=function(){for(var a=0,b=0,c=0,d=0,e;e=this.elements[d];d++)b+=e.width,Blockly.blockRendering.Types.isSpacer(e)||(Blockly.blockRendering.Types.isNextConnection(e)?c=Math.max(c,e.height):a=Math.max(a,e.height));this.width=Math.max(this.minWidth,b);this.height=Math.max(this.minHeight,a)+c;this.descenderHeight=c;this.widthWithConnectedBlocks=this.width};Blockly.blockRendering.BottomRow.prototype.startsWithElemSpacer=function(){return!1}; -Blockly.blockRendering.SpacerRow=function(a,b,c){Blockly.blockRendering.SpacerRow.superClass_.constructor.call(this,a);this.type=this.type|Blockly.blockRendering.Types.SPACER|Blockly.blockRendering.Types.BETWEEN_ROW_SPACER;this.width=c;this.height=b;this.followsStatement=!1;this.widthWithConnectedBlocks=0;this.elements=[new Blockly.blockRendering.InRowSpacer(this.constants_,c)]};Blockly.utils.object.inherits(Blockly.blockRendering.SpacerRow,Blockly.blockRendering.Row); -Blockly.blockRendering.SpacerRow.prototype.measure=function(){};Blockly.blockRendering.InputRow=function(a){Blockly.blockRendering.InputRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.INPUT_ROW;this.connectedBlockWidths=0};Blockly.utils.object.inherits(Blockly.blockRendering.InputRow,Blockly.blockRendering.Row); +Blockly.blockRendering.BottomRow.prototype.endsWithElemSpacer=function(){return!1};Blockly.blockRendering.SpacerRow=function(a,b,c){Blockly.blockRendering.SpacerRow.superClass_.constructor.call(this,a);this.type=this.type|Blockly.blockRendering.Types.SPACER|Blockly.blockRendering.Types.BETWEEN_ROW_SPACER;this.width=c;this.height=b;this.followsStatement=!1;this.widthWithConnectedBlocks=0;this.elements=[new Blockly.blockRendering.InRowSpacer(this.constants_,c)]}; +Blockly.utils.object.inherits(Blockly.blockRendering.SpacerRow,Blockly.blockRendering.Row);Blockly.blockRendering.SpacerRow.prototype.measure=function(){};Blockly.blockRendering.InputRow=function(a){Blockly.blockRendering.InputRow.superClass_.constructor.call(this,a);this.type|=Blockly.blockRendering.Types.INPUT_ROW;this.connectedBlockWidths=0};Blockly.utils.object.inherits(Blockly.blockRendering.InputRow,Blockly.blockRendering.Row); Blockly.blockRendering.InputRow.prototype.measure=function(){this.width=this.minWidth;this.height=this.minHeight;for(var a=0,b=0,c;c=this.elements[b];b++)this.width+=c.width,Blockly.blockRendering.Types.isInput(c)&&(Blockly.blockRendering.Types.isStatementInput(c)?a+=c.connectedBlockWidth:Blockly.blockRendering.Types.isExternalInput(c)&&0!=c.connectedBlockWidth&&(a+=c.connectedBlockWidth-c.connectionWidth)),Blockly.blockRendering.Types.isSpacer(c)||(this.height=Math.max(this.height,c.height));this.connectedBlockWidths= -a;this.widthWithConnectedBlocks=this.width+a};Blockly.blockRendering.InputRow.prototype.endsWithElemSpacer=function(){return!this.hasExternalInput&&!this.hasStatement};Blockly.blockRendering.RenderInfo=function(a,b){this.block_=b;this.renderer_=a;this.constants_=this.renderer_.getConstants();this.outputConnection=b.outputConnection?new Blockly.blockRendering.OutputConnection(this.constants_,b.outputConnection):null;this.isInline=b.getInputsInline()&&!b.isCollapsed();this.isCollapsed=b.isCollapsed();this.isInsertionMarker=b.isInsertionMarker();this.RTL=b.RTL;this.statementEdge=this.width=this.widthWithChildren=this.height=0;this.rows=[];this.hiddenIcons=[];this.topRow= -new Blockly.blockRendering.TopRow(this.constants_);this.bottomRow=new Blockly.blockRendering.BottomRow(this.constants_);this.startY=this.startX=0};Blockly.blockRendering.RenderInfo.prototype.getRenderer=function(){return this.renderer_};Blockly.blockRendering.RenderInfo.prototype.measure=function(){this.createRows_();this.addElemSpacing_();this.computeBounds_();this.alignRowElements_();this.addRowSpacing_();this.finalize_()}; -Blockly.blockRendering.RenderInfo.prototype.createRows_=function(){this.populateTopRow_();this.rows.push(this.topRow);var a=new Blockly.blockRendering.InputRow(this.constants_),b=this.block_.getIcons();if(b.length)for(var c=0,d;d=b[c];c++){var e=new Blockly.blockRendering.Icon(this.constants_,d);this.isCollapsed&&d.collapseHidden?this.hiddenIcons.push(e):a.elements.push(e)}d=null;for(c=0;b=this.block_.inputList[c];c++)if(b.isVisible()){this.shouldStartNewRow_(b,d)&&(this.rows.push(a),a=new Blockly.blockRendering.InputRow(this.constants_)); -for(d=0;e=b.fieldRow[d];d++)a.elements.push(new Blockly.blockRendering.Field(this.constants_,e,b));this.addInput_(b,a);d=b}this.isCollapsed&&(a.hasJaggedEdge=!0,a.elements.push(new Blockly.blockRendering.JaggedEdge(this.constants_)));(a.elements.length||a.hasDummyInput)&&this.rows.push(a);this.populateBottomRow_();this.rows.push(this.bottomRow)}; -Blockly.blockRendering.RenderInfo.prototype.populateTopRow_=function(){var a=!!this.block_.previousConnection,b=(this.block_.hat?"cap"===this.block_.hat:Blockly.BlockSvg.START_HAT)&&!this.outputConnection&&!a;this.topRow.hasLeftSquareCorner(this.block_)?this.topRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_)):this.topRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_));b?(a=new Blockly.blockRendering.Hat(this.constants_),this.topRow.elements.push(a), -this.topRow.capline=a.ascenderHeight):a&&(this.topRow.hasPreviousConnection=!0,this.topRow.connection=new Blockly.blockRendering.PreviousConnection(this.constants_,this.block_.previousConnection),this.topRow.elements.push(this.topRow.connection));this.block_.inputList.length&&this.block_.inputList[0].type==Blockly.NEXT_STATEMENT&&!this.block_.isCollapsed()?this.topRow.minHeight=this.constants_.LARGE_PADDING:this.topRow.minHeight=this.constants_.MEDIUM_PADDING}; -Blockly.blockRendering.RenderInfo.prototype.populateBottomRow_=function(){this.bottomRow.hasNextConnection=!!this.block_.nextConnection;this.bottomRow.minHeight=this.block_.inputList.length&&this.block_.inputList[this.block_.inputList.length-1].type==Blockly.NEXT_STATEMENT?this.constants_.LARGE_PADDING:this.constants_.MEDIUM_PADDING-1;this.bottomRow.hasLeftSquareCorner(this.block_)?this.bottomRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_)):this.bottomRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_)); -this.bottomRow.hasNextConnection&&(this.bottomRow.connection=new Blockly.blockRendering.NextConnection(this.constants_,this.block_.nextConnection),this.bottomRow.elements.push(this.bottomRow.connection))}; +a;this.widthWithConnectedBlocks=this.width+a};Blockly.blockRendering.InputRow.prototype.endsWithElemSpacer=function(){return!this.hasExternalInput&&!this.hasStatement};Blockly.blockRendering.RenderInfo=function(a,b){this.block_=b;this.renderer_=a;this.constants_=this.renderer_.getConstants();this.outputConnection=b.outputConnection?new Blockly.blockRendering.OutputConnection(this.constants_,b.outputConnection):null;this.isInline=b.getInputsInline()&&!b.isCollapsed();this.isCollapsed=b.isCollapsed();this.isInsertionMarker=b.isInsertionMarker();this.RTL=b.RTL;this.statementEdge=this.width=this.widthWithChildren=this.height=0;this.rows=[];this.inputRowNum_=1;this.hiddenIcons= +[];this.topRow=new Blockly.blockRendering.TopRow(this.constants_);this.bottomRow=new Blockly.blockRendering.BottomRow(this.constants_);this.startY=this.startX=0};Blockly.blockRendering.RenderInfo.prototype.getRenderer=function(){return this.renderer_};Blockly.blockRendering.RenderInfo.prototype.measure=function(){this.createRows_();this.addElemSpacing_();this.addRowSpacing_();this.computeBounds_();this.alignRowElements_();this.finalize_()}; +Blockly.blockRendering.RenderInfo.prototype.createRows_=function(){this.populateTopRow_();this.rows.push(this.topRow);var a=new Blockly.blockRendering.InputRow(this.constants_),b=this.block_.getIcons();if(b.length)for(var c=0,d;d=b[c];c++){var e=new Blockly.blockRendering.Icon(this.constants_,d);this.isCollapsed&&d.collapseHidden?this.hiddenIcons.push(e):a.elements.push(e)}d=null;for(c=0;b=this.block_.inputList[c];c++)if(b.isVisible()){this.shouldStartNewRow_(b,d)&&(this.rows.push(a),a=new Blockly.blockRendering.InputRow(this.constants_), +this.inputRowNum_++);for(d=0;e=b.fieldRow[d];d++)a.elements.push(new Blockly.blockRendering.Field(this.constants_,e,b));this.addInput_(b,a);d=b}this.isCollapsed&&(a.hasJaggedEdge=!0,a.elements.push(new Blockly.blockRendering.JaggedEdge(this.constants_)));(a.elements.length||a.hasDummyInput)&&this.rows.push(a);this.populateBottomRow_();this.rows.push(this.bottomRow)}; +Blockly.blockRendering.RenderInfo.prototype.populateTopRow_=function(){var a=!!this.block_.previousConnection,b=("undefined"!==typeof this.block_.hat?"cap"===this.block_.hat:this.constants_.ADD_START_HATS)&&!this.outputConnection&&!a;this.topRow.hasLeftSquareCorner(this.block_)?this.topRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_)):this.topRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_));b?(a=new Blockly.blockRendering.Hat(this.constants_),this.topRow.elements.push(a), +this.topRow.capline=a.ascenderHeight):a&&(this.topRow.hasPreviousConnection=!0,this.topRow.connection=new Blockly.blockRendering.PreviousConnection(this.constants_,this.block_.previousConnection),this.topRow.elements.push(this.topRow.connection));this.block_.inputList.length&&this.block_.inputList[0].type==Blockly.NEXT_STATEMENT&&!this.block_.isCollapsed()?this.topRow.minHeight=this.constants_.TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT:this.topRow.minHeight=this.constants_.TOP_ROW_MIN_HEIGHT;this.topRow.hasRightSquareCorner(this.block_)? +this.topRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_,"right")):this.topRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_,"right"))}; +Blockly.blockRendering.RenderInfo.prototype.populateBottomRow_=function(){this.bottomRow.hasNextConnection=!!this.block_.nextConnection;this.bottomRow.minHeight=this.block_.inputList.length&&this.block_.inputList[this.block_.inputList.length-1].type==Blockly.NEXT_STATEMENT?this.constants_.BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT:this.constants_.BOTTOM_ROW_MIN_HEIGHT;this.bottomRow.hasLeftSquareCorner(this.block_)?this.bottomRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_)): +this.bottomRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_));this.bottomRow.hasNextConnection&&(this.bottomRow.connection=new Blockly.blockRendering.NextConnection(this.constants_,this.block_.nextConnection),this.bottomRow.elements.push(this.bottomRow.connection));this.bottomRow.hasRightSquareCorner(this.block_)?this.bottomRow.elements.push(new Blockly.blockRendering.SquareCorner(this.constants_,"right")):this.bottomRow.elements.push(new Blockly.blockRendering.RoundCorner(this.constants_, +"right"))}; Blockly.blockRendering.RenderInfo.prototype.addInput_=function(a,b){this.isInline&&a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.InlineInput(this.constants_,a)),b.hasInlineInput=!0):a.type==Blockly.NEXT_STATEMENT?(b.elements.push(new Blockly.blockRendering.StatementInput(this.constants_,a)),b.hasStatement=!0):a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.ExternalValueInput(this.constants_,a)),b.hasExternalInput=!0):a.type==Blockly.DUMMY_INPUT&&(b.minHeight= -Math.max(b.minHeight,this.constants_.DUMMY_INPUT_MIN_HEIGHT),b.hasDummyInput=!0);b.align=a.align};Blockly.blockRendering.RenderInfo.prototype.shouldStartNewRow_=function(a,b){return b?a.type==Blockly.NEXT_STATEMENT||b.type==Blockly.NEXT_STATEMENT?!0:a.type==Blockly.INPUT_VALUE||a.type==Blockly.DUMMY_INPUT?!this.isInline:!1:!1}; -Blockly.blockRendering.RenderInfo.prototype.addElemSpacing_=function(){for(var a=0,b;b=this.rows[a];a++){var c=b.elements;b.elements=[];b.startsWithElemSpacer()&&b.elements.push(new Blockly.blockRendering.InRowSpacer(this.constants_,this.getInRowSpacing_(null,c[0])));for(var d=0;d>>/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: #e4e4e4;","}",".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_=[];if(!(0>=this.workspace_.options.maxTrashcanContents)){a={scrollbars:!0,disabledPatternId:this.workspace_.options.disabledPatternId,parentWorkspace:this.workspace_,RTL:this.workspace_.RTL,oneBasedIndex:this.workspace_.options.oneBasedIndex,renderer:this.workspace_.options.renderer};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.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; -Blockly.Trashcan.prototype.createDom=function(){this.svgGroup_=Blockly.utils.dom.createSvgElement("g",{"class":"blocklyTrash"},null);var a=String(Math.random()).substring(2);var b=Blockly.utils.dom.createSvgElement("clipPath",{id:"blocklyTrashBodyClipPath"+a},this.svgGroup_);Blockly.utils.dom.createSvgElement("rect",{width:this.WIDTH_,height:this.BODY_HEIGHT_,y:this.LID_HEIGHT_},b);var c=Blockly.utils.dom.createSvgElement("image",{width:Blockly.SPRITE.width,x:-this.SPRITE_LEFT_,height:Blockly.SPRITE.height, -y:-this.SPRITE_TOP_,"clip-path":"url(#blocklyTrashBodyClipPath"+a+")"},this.svgGroup_);c.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.workspace_.options.pathToMedia+Blockly.SPRITE.url);b=Blockly.utils.dom.createSvgElement("clipPath",{id:"blocklyTrashLidClipPath"+a},this.svgGroup_);Blockly.utils.dom.createSvgElement("rect",{width:this.WIDTH_,height:this.LID_HEIGHT_},b);this.svgLid_=Blockly.utils.dom.createSvgElement("image",{width:Blockly.SPRITE.width,x:-this.SPRITE_LEFT_,height:Blockly.SPRITE.height, -y:-this.SPRITE_TOP_,"clip-path":"url(#blocklyTrashLidClipPath"+a+")"},this.svgGroup_);this.svgLid_.setAttributeNS(Blockly.utils.dom.XLINK_NS,"xlink:href",this.workspace_.options.pathToMedia+Blockly.SPRITE.url);Blockly.bindEventWithChecks_(this.svgGroup_,"mouseup",this,this.click);Blockly.bindEvent_(c,"mouseover",this,this.mouseOver_);Blockly.bindEvent_(c,"mouseout",this,this.mouseOut_);this.animateLid_();return this.svgGroup_}; -Blockly.Trashcan.prototype.init=function(a){0this.minOpenness_&&1>this.lidOpen_&&(this.lidTask_=setTimeout(this.animateLid_.bind(this),20))}; -Blockly.Trashcan.prototype.setLidAngle_=function(a){var b=this.workspace_.toolboxPosition==Blockly.TOOLBOX_AT_RIGHT||this.workspace_.horizontalLayout&&this.workspace_.RTL;this.svgLid_.setAttribute("transform","rotate("+(b?-a:a)+","+(b?4:this.WIDTH_-4)+","+(this.LID_HEIGHT_-2)+")")};Blockly.Trashcan.prototype.close=function(){this.setOpen_(!1)};Blockly.Trashcan.prototype.click=function(){if(this.contents_.length){for(var a=[],b=0,c;c=this.contents_[b];b++)a[b]=Blockly.Xml.textToDom(c);this.flyout_.show(a)}}; -Blockly.Trashcan.prototype.mouseOver_=function(){this.contents_.length&&this.setOpen_(!0)};Blockly.Trashcan.prototype.mouseOut_=function(){this.setOpen_(!1)}; -Blockly.Trashcan.prototype.onDelete_=function(a){if(!(0>=this.workspace_.options.maxTrashcanContents)&&a.type==Blockly.Events.BLOCK_DELETE&&"shadow"!=a.oldXml.tagName.toLowerCase()&&(a=this.cleanBlockXML_(a.oldXml),-1==this.contents_.indexOf(a))){for(this.contents_.unshift(a);this.contents_.length>this.workspace_.options.maxTrashcanContents;)this.contents_.pop();this.minOpenness_=this.HAS_BLOCKS_LID_ANGLE;this.setLidAngle_(45*this.minOpenness_)}}; -Blockly.Trashcan.prototype.cleanBlockXML_=function(a){for(var b=a=a.cloneNode(!0);b;){b.removeAttribute&&(b.removeAttribute("x"),b.removeAttribute("y"),b.removeAttribute("id"));var c=b.firstChild||b.nextSibling;if(!c)for(c=b.parentNode;c;){if(c.nextSibling){c=c.nextSibling;break}c=c.parentNode}b=c}return Blockly.Xml.domToText(a)};Blockly.VariablesDynamic={};Blockly.VariablesDynamic.onCreateVariableButtonClick_String=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),null,"String")};Blockly.VariablesDynamic.onCreateVariableButtonClick_Number=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),null,"Number")};Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour=function(a){Blockly.Variables.createVariableButtonHandler(a.getTargetWorkspace(),null,"Colour")}; -Blockly.VariablesDynamic.flyoutCategory=function(a){var b=[],c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_STRING_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_STRING");b.push(c);c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_NUMBER_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_NUMBER");b.push(c);c=document.createElement("button");c.setAttribute("text",Blockly.Msg.NEW_COLOUR_VARIABLE);c.setAttribute("callbackKey","CREATE_VARIABLE_COLOUR"); -b.push(c);a.registerButtonCallback("CREATE_VARIABLE_STRING",Blockly.VariablesDynamic.onCreateVariableButtonClick_String);a.registerButtonCallback("CREATE_VARIABLE_NUMBER",Blockly.VariablesDynamic.onCreateVariableButtonClick_Number);a.registerButtonCallback("CREATE_VARIABLE_COLOUR",Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour);a=Blockly.VariablesDynamic.flyoutCategoryBlocks(a);return b=b.concat(a)}; -Blockly.VariablesDynamic.flyoutCategoryBlocks=function(a){a=a.getAllVariables();var b=[];if(0image, .blocklyZoom>svg>image {","opacity: .4;","}",".blocklyZoom>image:hover, .blocklyZoom>svg>image:hover {","opacity: .6;","}",".blocklyZoom>image:active, .blocklyZoom>svg>image:active {","opacity: .8;","}"]);Blockly.Themes.Dark={}; +a+.5);return{path:function(a){return a?b:c}}};Blockly.geras.PathObject=function(a,b,c){this.constants_=c;this.svgRoot=a;this.svgPathDark=Blockly.utils.dom.createSvgElement("path",{"class":"blocklyPathDark",transform:"translate(1,1)"},this.svgRoot);this.svgPath=Blockly.utils.dom.createSvgElement("path",{"class":"blocklyPath"},this.svgRoot);this.svgPathLight=Blockly.utils.dom.createSvgElement("path",{"class":"blocklyPathLight"},this.svgRoot);this.colourDark="#000000";this.style=b};Blockly.utils.object.inherits(Blockly.geras.PathObject,Blockly.blockRendering.PathObject); +Blockly.geras.PathObject.prototype.setPath=function(a){this.svgPath.setAttribute("d",a);this.svgPathDark.setAttribute("d",a)};Blockly.geras.PathObject.prototype.setHighlightPath=function(a){this.svgPathLight.setAttribute("d",a)};Blockly.geras.PathObject.prototype.flipRTL=function(){this.svgPath.setAttribute("transform","scale(-1 1)");this.svgPathLight.setAttribute("transform","scale(-1 1)");this.svgPathDark.setAttribute("transform","translate(1,1) scale(-1 1)")}; +Blockly.geras.PathObject.prototype.applyColour=function(a){this.svgPathLight.style.display="";this.svgPathDark.style.display="";this.svgPathLight.setAttribute("stroke",this.style.colourTertiary);this.svgPathDark.setAttribute("fill",this.colourDark);Blockly.geras.PathObject.superClass_.applyColour.call(this,a);this.svgPath.setAttribute("stroke","none")}; +Blockly.geras.PathObject.prototype.setStyle=function(a){this.style=a;this.colourDark=Blockly.utils.colour.blend("#000",this.style.colourPrimary,.2)||this.colourDark};Blockly.geras.PathObject.prototype.updateHighlighted=function(a){a?(this.svgPath.setAttribute("filter","url(#"+this.constants_.embossFilterId+")"),this.svgPathLight.style.display="none"):(this.svgPath.setAttribute("filter","none"),this.svgPathLight.style.display="inline")}; +Blockly.geras.PathObject.prototype.updateShadow_=function(a){a&&(this.svgPathLight.style.display="none",this.svgPathDark.setAttribute("fill",this.style.colourSecondary),this.svgPath.setAttribute("stroke","none"),this.svgPath.setAttribute("fill",this.style.colourSecondary))};Blockly.geras.PathObject.prototype.updateDisabled_=function(a){Blockly.geras.PathObject.superClass_.updateDisabled_.call(this,a);a&&this.svgPath.setAttribute("stroke","none")};Blockly.geras.Renderer=function(a){Blockly.geras.Renderer.superClass_.constructor.call(this,a);this.highlightConstants_=null};Blockly.utils.object.inherits(Blockly.geras.Renderer,Blockly.blockRendering.Renderer);Blockly.geras.Renderer.prototype.init=function(){Blockly.geras.Renderer.superClass_.init.call(this);this.highlightConstants_=this.makeHighlightConstants_()};Blockly.geras.Renderer.prototype.makeConstants_=function(){return new Blockly.geras.ConstantProvider}; +Blockly.geras.Renderer.prototype.makeRenderInfo_=function(a){return new Blockly.geras.RenderInfo(this,a)};Blockly.geras.Renderer.prototype.makeDrawer_=function(a,b){return new Blockly.geras.Drawer(a,b)};Blockly.geras.Renderer.prototype.makePathObject=function(a,b){return new Blockly.geras.PathObject(a,b,this.getConstants())};Blockly.geras.Renderer.prototype.makeHighlightConstants_=function(){return new Blockly.geras.HighlightConstantProvider(this.getConstants())}; +Blockly.geras.Renderer.prototype.getHighlightConstants=function(){return this.highlightConstants_};Blockly.blockRendering.register("geras",Blockly.geras.Renderer);Blockly.thrasos={};Blockly.thrasos.RenderInfo=function(a,b){Blockly.thrasos.RenderInfo.superClass_.constructor.call(this,a,b)};Blockly.utils.object.inherits(Blockly.thrasos.RenderInfo,Blockly.blockRendering.RenderInfo);Blockly.thrasos.RenderInfo.prototype.getRenderer=function(){return this.renderer_}; +Blockly.thrasos.RenderInfo.prototype.addElemSpacing_=function(){for(var a=!1,b=0,c;c=this.rows[b];b++)c.hasExternalInput&&(a=!0);for(b=0;c=this.rows[b];b++){var d=c.elements;c.elements=[];c.startsWithElemSpacer()&&c.elements.push(new Blockly.blockRendering.InRowSpacer(this.constants_,this.getInRowSpacing_(null,d[0])));for(var e=0;erect: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+" .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: #fff200;","}",a+" .blocklyDisabled > .blocklyOutlinePath {","fill: url(#blocklyDisabledPattern"+this.randomIdentifier_+")","}"]};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=("undefined"!==typeof 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};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};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.rightSide=this.outputConnection?new Blockly.zelos.RightConnectionShape(this.constants_):null};Blockly.utils.object.inherits(Blockly.zelos.RenderInfo,Blockly.blockRendering.RenderInfo); +Blockly.zelos.RenderInfo.prototype.getRenderer=function(){return this.renderer_};Blockly.zelos.RenderInfo.prototype.measure=function(){this.createRows_();this.addElemSpacing_();this.addRowSpacing_();this.adjustXPosition_();this.computeBounds_();this.alignRowElements_();this.finalize_()}; +Blockly.zelos.RenderInfo.prototype.shouldStartNewRow_=function(a,b){return b?a.type==Blockly.NEXT_STATEMENT||b.type==Blockly.NEXT_STATEMENT?!0:a.type==Blockly.INPUT_VALUE||a.type==Blockly.DUMMY_INPUT?!this.isInline||this.isMultiRow:!1:!1};Blockly.zelos.RenderInfo.prototype.getDesiredRowWidth_=function(a){return a.hasStatement?this.width-this.startX-(this.constants_.INSIDE_CORNERS.rightWidth||0):Blockly.zelos.RenderInfo.superClass_.getDesiredRowWidth_.call(this,a)}; +Blockly.zelos.RenderInfo.prototype.getInRowSpacing_=function(a,b){return a&&b||!this.outputConnection||!this.outputConnection.isDynamicShape?!a&&b&&Blockly.blockRendering.Types.isStatementInput(b)?this.constants_.STATEMENT_INPUT_PADDING_LEFT:a&&Blockly.blockRendering.Types.isLeftRoundedCorner(a)&&b&&(Blockly.blockRendering.Types.isPreviousConnection(b)||Blockly.blockRendering.Types.isNextConnection(b))?b.notchOffset-this.constants_.CORNER_RADIUS:a&&Blockly.blockRendering.Types.isLeftSquareCorner(a)&& +b&&Blockly.blockRendering.Types.isHat(b)?this.constants_.NO_PADDING:this.constants_.MEDIUM_PADDING:this.constants_.NO_PADDING}; +Blockly.zelos.RenderInfo.prototype.getSpacerRowHeight_=function(a,b){if(Blockly.blockRendering.Types.isTopRow(a)&&Blockly.blockRendering.Types.isBottomRow(b))return this.constants_.EMPTY_BLOCK_SPACER_HEIGHT;var c=Blockly.blockRendering.Types.isInputRow(a)&&a.hasStatement,d=Blockly.blockRendering.Types.isInputRow(b)&&b.hasStatement;return d||c?(a=Math.max(this.constants_.MEDIUM_PADDING,Math.max(this.constants_.NOTCH_HEIGHT,this.constants_.INSIDE_CORNERS.rightHeight||0)),d&&c?Math.max(a,this.constants_.DUMMY_INPUT_MIN_HEIGHT): +a):Blockly.blockRendering.Types.isTopRow(a)?a.hasPreviousConnection||this.outputConnection?this.constants_.NO_PADDING:this.constants_.SMALL_PADDING:Blockly.blockRendering.Types.isBottomRow(b)?this.outputConnection?this.constants_.NO_PADDING:this.constants_.SMALL_PADDING:this.constants_.MEDIUM_PADDING}; +Blockly.zelos.RenderInfo.prototype.getSpacerRowWidth_=function(a,b){var c=this.width-this.startX;return Blockly.blockRendering.Types.isInputRow(a)&&a.hasStatement||Blockly.blockRendering.Types.isInputRow(b)&&b.hasStatement?Math.max(c,this.constants_.STATEMENT_INPUT_SPACER_MIN_WIDTH):c}; +Blockly.zelos.RenderInfo.prototype.getElemCenterline_=function(a,b){return a.hasStatement&&!Blockly.blockRendering.Types.isSpacer(b)?a.yPos+this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT/2:Blockly.zelos.RenderInfo.superClass_.getElemCenterline_.call(this,a,b)}; +Blockly.zelos.RenderInfo.prototype.addInput_=function(a,b){a.type==Blockly.DUMMY_INPUT&&b.hasDummyInput&&b.align==Blockly.ALIGN_LEFT&&a.align==Blockly.ALIGN_RIGHT&&(b.rightAlignedDummyInput=a);this.isInline&&a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.InlineInput(this.constants_,a)),b.hasInlineInput=!0):a.type==Blockly.NEXT_STATEMENT?(b.elements.push(new Blockly.zelos.StatementInput(this.constants_,a)),b.hasStatement=!0):a.type==Blockly.INPUT_VALUE?(b.elements.push(new Blockly.blockRendering.ExternalValueInput(this.constants_, +a)),b.hasExternalInput=!0):a.type==Blockly.DUMMY_INPUT&&(b.minHeight=Math.max(b.minHeight,a.getSourceBlock()&&a.getSourceBlock().isShadow()?this.constants_.DUMMY_INPUT_SHADOW_MIN_HEIGHT:this.constants_.DUMMY_INPUT_MIN_HEIGHT),b.hasDummyInput=!0);null==b.align&&(b.align=a.align)}; +Blockly.zelos.RenderInfo.prototype.addAlignmentPadding_=function(a,b){if(a.rightAlignedDummyInput){for(var c,d=0,e;(e=a.elements[d])&&(Blockly.blockRendering.Types.isSpacer(e)&&(c=e),!Blockly.blockRendering.Types.isField(e)||e.parentInput!=a.rightAlignedDummyInput);d++);if(c){c.width+=b;a.width+=b;return}}Blockly.zelos.RenderInfo.superClass_.addAlignmentPadding_.call(this,a,b)}; +Blockly.zelos.RenderInfo.prototype.adjustXPosition_=function(){for(var a=this.constants_.NOTCH_OFFSET_LEFT+this.constants_.NOTCH_WIDTH,b=a,c=2;c=this.rows.length-1?!!this.bottomRow.hasNextConnection:!!f.precedesStatement;if(Blockly.blockRendering.Types.isInputRow(e)&&e.hasStatement)e.measure(),b=e.width-e.getLastInput().width+a;else if(d&&(2==c||f)&& +Blockly.blockRendering.Types.isInputRow(e)&&!e.hasStatement){f=e.xPos;d=null;for(var g=0,h;h=e.elements[g];g++)Blockly.blockRendering.Types.isSpacer(h)&&(d=h),!(d&&(Blockly.blockRendering.Types.isField(h)||Blockly.blockRendering.Types.isInput(h))&&f=this.rows.length-1?!!this.bottomRow.hasNextConnection:!!d.precedesStatement;if(e?this.topRow.hasPreviousConnection:b.followsStatement){var g=3==c.elements.length&&(c.elements[1].field instanceof Blockly.FieldLabel||c.elements[1].field instanceof Blockly.FieldImage);if(!e&&g)b.height-=this.constants_.SMALL_PADDING, +d.height-=this.constants_.SMALL_PADDING,c.height-=this.constants_.MEDIUM_PADDING;else if(!e&&!f)b.height+=this.constants_.SMALL_PADDING;else if(f){e=!1;for(f=0;g=c.elements[f];f++)if(Blockly.blockRendering.Types.isInlineInput(g)&&g.connectedBlock&&!g.connectedBlock.isShadow()&&40<=g.connectedBlock.getHeightWidth().height){e=!0;break}e&&(b.height-=this.constants_.SMALL_PADDING,d.height-=this.constants_.SMALL_PADDING)}}}}; +Blockly.zelos.RenderInfo.prototype.finalize_=function(){this.finalizeOutputConnection_();this.finalizeHorizontalAlignment_();this.finalizeVerticalAlignment_();Blockly.zelos.RenderInfo.superClass_.finalize_.call(this)};Blockly.zelos.Drawer=function(a,b){Blockly.zelos.Drawer.superClass_.constructor.call(this,a,b)};Blockly.utils.object.inherits(Blockly.zelos.Drawer,Blockly.blockRendering.Drawer); +Blockly.zelos.Drawer.prototype.draw=function(){var a=this.block_.pathObject;a.beginDrawing();this.hideHiddenIcons_();this.drawOutline_();this.drawInternals_();a.setPath(this.outlinePath_+"\n"+this.inlinePath_);this.info_.RTL&&a.flipRTL();Blockly.blockRendering.useDebugger&&this.block_.renderingDebugger.drawDebug(this.block_,this.info_);this.recordSizeOnBlock_();this.info_.outputConnection&&(a.outputShapeType=this.info_.outputConnection.shape.type);a.endDrawing()}; +Blockly.zelos.Drawer.prototype.drawOutline_=function(){this.info_.outputConnection&&this.info_.outputConnection.isDynamicShape?(this.drawFlatTop_(),this.drawRightDynamicConnection_(),this.drawFlatBottom_(),this.drawLeftDynamicConnection_()):Blockly.zelos.Drawer.superClass_.drawOutline_.call(this)}; +Blockly.zelos.Drawer.prototype.drawRightSideRow_=function(a){if(!(0>=a.height))if(a.precedesStatement||a.followsStatement){var b=this.constants_.INSIDE_CORNERS.rightHeight;b=a.height-(a.precedesStatement?b:0);this.outlinePath_+=(a.followsStatement?this.constants_.INSIDE_CORNERS.pathBottomRight:"")+(0e;e++){var f=1==e?b:c;f&&!f.outputConnection.checkType_(d)&&(Blockly.Events.setGroup(a.group),d===this.prevParentConnection_?(this.unplug(),d.getSourceBlock().bumpNeighbours()):(f.unplug(),f.bumpNeighbours()),Blockly.Events.setGroup(!1))}this.prevParentConnection_= -d}};Blockly.Extensions.registerMixin("logic_ternary",Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN);Blockly.Blocks.loops={};Blockly.Constants.Loops={};Blockly.Constants.Loops.HUE=120; +Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN={prevParentConnection_:null,onchange:function(a){var b=this.getInputTargetBlock("THEN"),c=this.getInputTargetBlock("ELSE"),e=this.outputConnection.targetConnection;if((b||c)&&e)for(var d=0;2>d;d++){var f=1==d?b:c;f&&!f.outputConnection.checkType(e)&&(Blockly.Events.setGroup(a.group),e===this.prevParentConnection_?(this.unplug(),e.getSourceBlock().bumpNeighbours()):(f.unplug(),f.bumpNeighbours()),Blockly.Events.setGroup(!1))}this.prevParentConnection_= +e}};Blockly.Extensions.registerMixin("logic_ternary",Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN);Blockly.Blocks.loops={};Blockly.Constants.Loops={};Blockly.Constants.Loops.HUE=120; Blockly.defineBlocksWithJsonArray([{type:"controls_repeat_ext",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"input_value",name:"TIMES",check:"Number"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_repeat",message0:"%{BKY_CONTROLS_REPEAT_TITLE}",args0:[{type:"field_number",name:"TIMES",value:10, min:0,precision:1}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",tooltip:"%{BKY_CONTROLS_REPEAT_TOOLTIP}",helpUrl:"%{BKY_CONTROLS_REPEAT_HELPURL}"},{type:"controls_whileUntil",message0:"%1 %2",args0:[{type:"field_dropdown",name:"MODE",options:[["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_WHILE}","WHILE"],["%{BKY_CONTROLS_WHILEUNTIL_OPERATOR_UNTIL}","UNTIL"]]},{type:"input_value",name:"BOOL",check:"Boolean"}], message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_WHILEUNTIL_HELPURL}",extensions:["controls_whileUntil_tooltip"]},{type:"controls_for",message0:"%{BKY_CONTROLS_FOR_TITLE}",args0:[{type:"field_variable",name:"VAR",variable:null},{type:"input_value",name:"FROM",check:"Number",align:"RIGHT"},{type:"input_value",name:"TO",check:"Number",align:"RIGHT"},{type:"input_value",name:"BY", @@ -66,7 +66,7 @@ check:"Number",align:"RIGHT"}],message1:"%{BKY_CONTROLS_REPEAT_INPUT_DO} %1",arg args1:[{type:"input_statement",name:"DO"}],previousStatement:null,nextStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FOREACH_HELPURL}",extensions:["contextMenu_newGetVariableBlock","controls_forEach_tooltip"]},{type:"controls_flow_statements",message0:"%1",args0:[{type:"field_dropdown",name:"FLOW",options:[["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_BREAK}","BREAK"],["%{BKY_CONTROLS_FLOW_STATEMENTS_OPERATOR_CONTINUE}","CONTINUE"]]}],previousStatement:null,style:"loop_blocks",helpUrl:"%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}", extensions:["controls_flow_tooltip","controls_flow_in_loop_check"]}]);Blockly.Constants.Loops.WHILE_UNTIL_TOOLTIPS={WHILE:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_WHILE}",UNTIL:"%{BKY_CONTROLS_WHILEUNTIL_TOOLTIP_UNTIL}"};Blockly.Extensions.register("controls_whileUntil_tooltip",Blockly.Extensions.buildTooltipForDropdown("MODE",Blockly.Constants.Loops.WHILE_UNTIL_TOOLTIPS));Blockly.Constants.Loops.BREAK_CONTINUE_TOOLTIPS={BREAK:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_BREAK}",CONTINUE:"%{BKY_CONTROLS_FLOW_STATEMENTS_TOOLTIP_CONTINUE}"}; Blockly.Extensions.register("controls_flow_tooltip",Blockly.Extensions.buildTooltipForDropdown("FLOW",Blockly.Constants.Loops.BREAK_CONTINUE_TOOLTIPS)); -Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getField("VAR").getVariable(),c=b.name;if(!this.isCollapsed()&&null!=c){var d={enabled:!0};d.text=Blockly.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c);b=Blockly.Variables.generateVariableFieldDom(b);c=Blockly.utils.xml.createElement("block");c.setAttribute("type","variables_get");c.appendChild(b);d.callback=Blockly.ContextMenu.callbackFactory(this,c);a.push(d)}}}}; +Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN={customContextMenu:function(a){if(!this.isInFlyout){var b=this.getField("VAR").getVariable(),c=b.name;if(!this.isCollapsed()&&null!=c){var e={enabled:!0};e.text=Blockly.Msg.VARIABLES_SET_CREATE_GET.replace("%1",c);b=Blockly.Variables.generateVariableFieldDom(b);c=Blockly.utils.xml.createElement("block");c.setAttribute("type","variables_get");c.appendChild(b);e.callback=Blockly.ContextMenu.callbackFactory(this,c);a.push(e)}}}}; Blockly.Extensions.registerMixin("contextMenu_newGetVariableBlock",Blockly.Constants.Loops.CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN);Blockly.Extensions.register("controls_for_tooltip",Blockly.Extensions.buildTooltipWithFieldText("%{BKY_CONTROLS_FOR_TOOLTIP}","VAR"));Blockly.Extensions.register("controls_forEach_tooltip",Blockly.Extensions.buildTooltipWithFieldText("%{BKY_CONTROLS_FOREACH_TOOLTIP}","VAR")); Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN={LOOP_TYPES:["controls_repeat","controls_repeat_ext","controls_forEach","controls_for","controls_whileUntil"],suppressPrefixSuffix:!0,getSurroundLoop:function(a){do{if(-1!=Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.LOOP_TYPES.indexOf(a.type))return a;a=a.getSurroundParent()}while(a);return null},onchange:function(a){this.workspace.isDragging&&!this.workspace.isDragging()&&(Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.getSurroundLoop(this)? (this.setWarningText(null),this.isInFlyout||this.setEnabled(!0)):(this.setWarningText(Blockly.Msg.CONTROLS_FLOW_STATEMENTS_WARNING),this.isInFlyout||this.getInheritedDisabled()||this.setEnabled(!1)))}};Blockly.Extensions.registerMixin("controls_flow_in_loop_check",Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN);Blockly.Blocks.math={};Blockly.Constants.Math={};Blockly.Constants.Math.HUE=230; @@ -90,36 +90,34 @@ Blockly.Constants.Math.LIST_MODES_MUTATOR_MIXIN={updateType_:function(a){"MODE"= Blockly.Extensions.registerMutator("math_modes_of_list_mutator",Blockly.Constants.Math.LIST_MODES_MUTATOR_MIXIN,Blockly.Constants.Math.LIST_MODES_MUTATOR_EXTENSION);Blockly.Blocks.procedures={}; Blockly.Blocks.procedures_defnoreturn={init:function(){var a=new Blockly.FieldTextInput("",Blockly.Procedures.rename);a.setSpellcheck(!1);this.appendDummyInput().appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_TITLE).appendField(a,"NAME").appendField("","PARAMS");this.setMutator(new Blockly.Mutator(["procedures_mutatorarg"]));(this.workspace.options.comments||this.workspace.options.parentWorkspace&&this.workspace.options.parentWorkspace.options.comments)&&Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT&&this.setCommentText(Blockly.Msg.PROCEDURES_DEFNORETURN_COMMENT); this.setStyle("procedure_blocks");this.setTooltip(Blockly.Msg.PROCEDURES_DEFNORETURN_TOOLTIP);this.setHelpUrl(Blockly.Msg.PROCEDURES_DEFNORETURN_HELPURL);this.arguments_=[];this.argumentVarModels_=[];this.setStatements_(!0);this.statementConnection_=null},setStatements_:function(a){this.hasStatements_!==a&&(a?(this.appendStatementInput("STACK").appendField(Blockly.Msg.PROCEDURES_DEFNORETURN_DO),this.getInput("RETURN")&&this.moveInputBefore("STACK","RETURN")):this.removeInput("STACK",!0),this.hasStatements_= -a)},updateParams_:function(){var a="";this.arguments_.length&&(a=Blockly.Msg.PROCEDURES_BEFORE_PARAMS+" "+this.arguments_.join(", "));Blockly.Events.disable();try{this.setFieldValue(a,"PARAMS")}finally{Blockly.Events.enable()}},mutationToDom:function(a){var b=Blockly.utils.xml.createElement("mutation");a&&b.setAttribute("name",this.getFieldValue("NAME"));for(var c=0;c} + */ +Blockly.Block.prototype.getDeveloperVariables; + /** * Dispose of this block. * @param {boolean} healStack If true, then try to heal any gap by connecting * the next statement with the previous statement. Otherwise, dispose of * all children of this block. + * @suppress {checkTypes} */ Blockly.Block.prototype.dispose = function(healStack) { if (!this.workspace) { @@ -316,11 +329,6 @@ Blockly.Block.prototype.dispose = function(healStack) { this.workspace.removeChangeListener(this.onchangeWrapper_); } - if (Blockly.keyboardAccessibilityMode) { - // No-op if this is called from the block_svg class. - Blockly.navigation.moveCursorOnBlockDelete(this); - } - this.unplug(healStack); if (Blockly.Events.isEnabled()) { Blockly.Events.fire(new Blockly.Events.BlockDelete(this)); @@ -334,7 +342,7 @@ Blockly.Block.prototype.dispose = function(healStack) { this.workspace.removeTopBlock(this); this.workspace.removeTypedBlock(this); // Remove from block database. - delete this.workspace.blockDB_[this.id]; + this.workspace.removeBlockById(this.id); this.workspace = null; } @@ -352,13 +360,13 @@ Blockly.Block.prototype.dispose = function(healStack) { } // Then dispose of myself. // Dispose of all inputs and their fields. - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { input.dispose(); } this.inputList.length = 0; // Dispose of any remaining connections (next/previous/output). var connections = this.getConnections_(true); - for (var i = 0, connection; connection = connections[i]; i++) { + for (var i = 0, connection; (connection = connections[i]); i++) { connection.dispose(); } } finally { @@ -377,8 +385,8 @@ Blockly.Block.prototype.dispose = function(healStack) { * @public */ Blockly.Block.prototype.initModel = function() { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { if (field.initModel) { field.initModel(); } @@ -433,7 +441,7 @@ Blockly.Block.prototype.unplugFromRow_ = function(opt_healStack) { // Disconnect the child block. childConnection.disconnect(); // Connect child to the parent if possible, otherwise bump away. - if (childConnection.checkType_(parentConnection)) { + if (childConnection.checkType(parentConnection)) { parentConnection.connect(childConnection); } else { childConnection.onFailedConnect(parentConnection); @@ -485,7 +493,7 @@ Blockly.Block.prototype.unplugFromStack_ = function(opt_healStack) { // Disconnect the next statement. var nextTarget = this.nextConnection.targetConnection; nextTarget.disconnect(); - if (previousTarget && previousTarget.checkType_(nextTarget)) { + if (previousTarget && previousTarget.checkType(nextTarget)) { // Attach the next statement to the previous statement. previousTarget.connect(nextTarget); } @@ -496,7 +504,7 @@ Blockly.Block.prototype.unplugFromStack_ = function(opt_healStack) { * Returns all connections originating from this block. * @param {boolean} _all If true, return all connections even hidden ones. * @return {!Array.} Array of connections. - * @private + * @package */ Blockly.Block.prototype.getConnections_ = function(_all) { var myConnections = []; @@ -509,7 +517,7 @@ Blockly.Block.prototype.getConnections_ = function(_all) { if (this.nextConnection) { myConnections.push(this.nextConnection); } - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.connection) { myConnections.push(input.connection); } @@ -560,7 +568,7 @@ Blockly.Block.prototype.getParent = function() { * @return {Blockly.Input} The input that connects to the specified block. */ Blockly.Block.prototype.getInputWithBlock = function(block) { - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.connection && input.connection.targetBlock() == block) { return input; } @@ -611,7 +619,7 @@ Blockly.Block.prototype.getPreviousBlock = function() { * @package */ Blockly.Block.prototype.getFirstStatementConnection = function() { - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.connection && input.connection.type == Blockly.NEXT_STATEMENT) { return input.connection; } @@ -639,7 +647,7 @@ Blockly.Block.prototype.getRootBlock = function() { * the top block of the sub stack. If we are nested in a statement input only * find the top-most nested block. Do not go all the way to the root block. * @return {!Blockly.Block} The top block in a stack. - * @private + * @package */ Blockly.Block.prototype.getTopStackBlock = function() { var block = this; @@ -662,7 +670,7 @@ Blockly.Block.prototype.getChildren = function(ordered) { return this.childBlocks_; } var blocks = []; - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.connection) { var child = input.connection.targetBlock(); if (child) { @@ -725,7 +733,7 @@ Blockly.Block.prototype.setParent = function(newParent) { Blockly.Block.prototype.getDescendants = function(ordered) { var blocks = [this]; var childBlocks = this.getChildren(ordered); - for (var child, i = 0; child = childBlocks[i]; i++) { + for (var child, i = 0; (child = childBlocks[i]); i++) { blocks.push.apply(blocks, child.getDescendants(ordered)); } return blocks; @@ -829,13 +837,21 @@ Blockly.Block.prototype.isEditable = function() { */ Blockly.Block.prototype.setEditable = function(editable) { this.editable_ = editable; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { field.updateEditable(); } } }; +/** + * Returns if this block has been disposed of / deleted. + * @return {boolean} True if this block has been disposed of / deleted. + */ +Blockly.Block.prototype.isDisposed = function() { + return this.disposed; +}; + /** * Find the connection on this block that corresponds to the given connection * on the other block. @@ -885,60 +901,6 @@ Blockly.Block.prototype.getColour = function() { return this.colour_; }; -/** - * Get the secondary colour of a block. - * @return {?string} #RRGGBB string. - */ -Blockly.Block.prototype.getColourSecondary = function() { - return this.colourSecondary_; -}; - -/** - * Get the tertiary colour of a block. - * @return {?string} #RRGGBB string. - */ -Blockly.Block.prototype.getColourTertiary = function() { - return this.colourTertiary_; -}; - -/** - * Get the shadow colour of a block. - * @return {?string} #RRGGBB string. - */ -Blockly.Block.prototype.getColourShadow = function() { - var colourSecondary = this.getColourSecondary(); - if (colourSecondary) { - return colourSecondary; - } - return Blockly.utils.colour.blend('#fff', this.getColour(), 0.6); -}; - -/** - * Get the border colour(s) of a block. - * @return {{colourDark, colourLight, colourBorder}} An object containing - * colour values for the border(s) of the block. If the block is using a - * style the colourBorder will be defined and equal to the tertiary colour - * of the style (#RRGGBB string). Otherwise the colourDark and colourLight - * attributes will be defined (#RRGGBB strings). - * @package - */ -Blockly.Block.prototype.getColourBorder = function() { - var colourTertiary = this.getColourTertiary(); - if (colourTertiary) { - return { - colourBorder: colourTertiary, - colourLight: null, - colourDark: null - }; - } - var colour = this.getColour(); - return { - colourBorder: null, - colourLight: Blockly.utils.colour.blend('#fff', colour, 0.3), - colourDark: Blockly.utils.colour.blend('#000', colour, 0.2) - }; -}; - /** * Get the name of the block style. * @return {?string} Name of the block style. @@ -961,48 +923,17 @@ Blockly.Block.prototype.getHue = function() { * or a message reference string pointing to one of those two values. */ Blockly.Block.prototype.setColour = function(colour) { - var dereferenced = (typeof colour == 'string') ? - Blockly.utils.replaceMessageReferences(colour) : colour; - - var hue = Number(dereferenced); - if (!isNaN(hue) && 0 <= hue && hue <= 360) { - this.hue_ = hue; - this.colour_ = Blockly.hueToHex(hue); - } else { - var hex = Blockly.utils.colour.parse(dereferenced); - if (hex) { - this.colour_ = hex; - // Only store hue if colour is set as a hue. - this.hue_ = null; - } else { - var errorMsg = 'Invalid colour: "' + dereferenced + '"'; - if (colour != dereferenced) { - errorMsg += ' (from "' + colour + '")'; - } - throw Error(errorMsg); - } - } + var parsed = Blockly.utils.parseBlockColour(colour); + this.hue_ = parsed.hue; + this.colour_ = parsed.hex; }; /** * Set the style and colour values of a block. * @param {string} blockStyleName Name of the block style - * @throws {Error} if the block style does not exist. */ Blockly.Block.prototype.setStyle = function(blockStyleName) { - var theme = this.workspace.getTheme(); - var blockStyle = theme.getBlockStyle(blockStyleName); this.styleName_ = blockStyleName; - - if (blockStyle) { - this.colourSecondary_ = blockStyle['colourSecondary']; - this.colourTertiary_ = blockStyle['colourTertiary']; - this.hat = blockStyle.hat; - // Set colour will trigger an updateColour() on a block_svg - this.setColour(blockStyle['colourPrimary']); - } else { - throw Error('Invalid style name: ' + blockStyleName); - } }; /** @@ -1034,8 +965,8 @@ Blockly.Block.prototype.setOnChange = function(onchangeFn) { * @return {Blockly.Field} Named field, or null if field does not exist. */ Blockly.Block.prototype.getField = function(name) { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { if (field.name == name) { return field; } @@ -1051,8 +982,8 @@ Blockly.Block.prototype.getField = function(name) { */ Blockly.Block.prototype.getVars = function() { var vars = []; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { if (field.referencesVariables()) { vars.push(field.getValue()); } @@ -1068,8 +999,8 @@ Blockly.Block.prototype.getVars = function() { */ Blockly.Block.prototype.getVarModels = function() { var vars = []; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { if (field.referencesVariables()) { var model = this.workspace.getVariableById( /** @type {string} */ (field.getValue())); @@ -1091,8 +1022,8 @@ Blockly.Block.prototype.getVarModels = function() { * @package */ Blockly.Block.prototype.updateVarName = function(variable) { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { if (field.referencesVariables() && variable.getId() == field.getValue()) { field.refreshVariableName(); @@ -1109,8 +1040,8 @@ Blockly.Block.prototype.updateVarName = function(variable) { * an updated name. */ Blockly.Block.prototype.renameVarById = function(oldId, newId) { - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { if (field.referencesVariables() && oldId == field.getValue()) { field.setValue(newId); @@ -1274,6 +1205,22 @@ Blockly.Block.prototype.getInputsInline = function() { return false; }; +/** + * Set the block's output shape. + * @param {?number} outputShape Value representing an output shape. + */ +Blockly.Block.prototype.setOutputShape = function(outputShape) { + this.outputShape_ = outputShape; +}; + +/** + * Get the block's output shape. + * @return {?number} Value representing output shape if one exists. + */ +Blockly.Block.prototype.getOutputShape = function() { + return this.outputShape_; +}; + /** * Set whether the block is disabled or not. * @param {boolean} disabled True if disabled. @@ -1355,8 +1302,8 @@ Blockly.Block.prototype.toString = function(opt_maxLength, 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++) { + 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) { @@ -1445,7 +1392,7 @@ Blockly.Block.prototype.jsonInit = function(json) { var i = 0; while (json['message' + i] !== undefined) { this.interpolate_(json['message' + i], json['args' + i] || [], - json['lastDummyAlign' + i]); + json['lastDummyAlign' + i], warningPrefix); i++; } @@ -1456,6 +1403,9 @@ Blockly.Block.prototype.jsonInit = function(json) { if (json['output'] !== undefined) { this.setOutput(true, json['output']); } + if (json['outputShape'] !== undefined) { + this.setOutputShape(json['outputShape']); + } if (json['previousStatement'] !== undefined) { this.setPreviousStatement(true, json['previousStatement']); } @@ -1569,9 +1519,11 @@ Blockly.Block.prototype.mixin = function(mixinObj, opt_disableCheck) { * @param {!Array} args Array of arguments to be interpolated. * @param {string|undefined} lastDummyAlign If a dummy input is added at the * end, how should it be aligned? + * @param {string} warningPrefix Warning prefix string identifying block. * @private */ -Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { +Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign, + warningPrefix) { var tokens = Blockly.utils.tokenizeInterpolation(message); // Interpolate the arguments. Build a list of elements. var indexDup = []; @@ -1616,7 +1568,8 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { var alignmentLookup = { 'LEFT': Blockly.ALIGN_LEFT, 'RIGHT': Blockly.ALIGN_RIGHT, - 'CENTRE': Blockly.ALIGN_CENTRE + 'CENTRE': Blockly.ALIGN_CENTRE, + 'CENTER': Blockly.ALIGN_CENTRE }; // Populate block with inputs and fields. var fieldStack = []; @@ -1648,11 +1601,9 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { field = Blockly.fieldRegistry.fromJson(element); // Unknown field. - if (!field) { - if (element['alt']) { - element = element['alt']; - altRepeat = true; - } + if (!field && element['alt']) { + element = element['alt']; + altRepeat = true; } } } @@ -1664,7 +1615,13 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { input.setCheck(element['check']); } if (element['align']) { - input.setAlign(alignmentLookup[element['align']]); + var alignment = alignmentLookup[element['align'].toUpperCase()]; + if (alignment === undefined) { + console.warn(warningPrefix + 'Illegal align value: ', + element['align']); + } else { + input.setAlign(alignment); + } } for (var j = 0; j < fieldStack.length; j++) { input.appendField(fieldStack[j][0], fieldStack[j][1]); @@ -1708,7 +1665,7 @@ Blockly.Block.prototype.moveInputBefore = function(name, refName) { // Find both inputs. var inputIndex = -1; var refIndex = refName ? -1 : this.inputList.length; - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.name == name) { inputIndex = i; if (refIndex != -1) { @@ -1761,11 +1718,10 @@ 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. - * @throws {Error} if the input is not present and - * opt_quiet is not true. + * @throws {Error} if the input is not present and opt_quiet is not true. */ Blockly.Block.prototype.removeInput = function(name, opt_quiet) { - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.name == name) { input.dispose(); this.inputList.splice(i, 1); @@ -1783,7 +1739,7 @@ Blockly.Block.prototype.removeInput = function(name, opt_quiet) { * @return {Blockly.Input} The input object, or null if input does not exist. */ Blockly.Block.prototype.getInput = function(name) { - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.name == name) { return input; } @@ -1805,7 +1761,7 @@ Blockly.Block.prototype.getInputTargetBlock = function(name) { /** * Returns the comment on this block (or null if there is no comment). - * @return {string} Block's comment. + * @return {?string} Block's comment. */ Blockly.Block.prototype.getCommentText = function() { return this.commentModel.text; @@ -1872,7 +1828,7 @@ Blockly.Block.prototype.moveBy = function(dx, dy) { * Create a connection of the specified type. * @param {number} type The type of the connection to create. * @return {!Blockly.Connection} A new connection of the specified type. - * @private + * @protected */ Blockly.Block.prototype.makeConnection_ = function(type) { return new Blockly.Connection(this, type); @@ -1895,7 +1851,7 @@ Blockly.Block.prototype.allInputsFilled = function(opt_shadowBlocksAreFilled) { } // Recursively check each input block of the current block. - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (!input.connection) { continue; } diff --git a/core/block_drag_surface.js b/core/block_drag_surface.js index ffdf54729..5c33b5273 100644 --- a/core/block_drag_surface.js +++ b/core/block_drag_surface.js @@ -173,7 +173,7 @@ Blockly.BlockDragSurfaceSvg.prototype.translateSurface = function(x, y) { * @return {!Blockly.utils.Coordinate} Current translation of the surface. */ Blockly.BlockDragSurfaceSvg.prototype.getSurfaceTranslation = function() { - var xy = Blockly.utils.getRelativeXY(this.SVG_); + var xy = Blockly.utils.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_)); return new Blockly.utils.Coordinate(xy.x / this.scale_, xy.y / this.scale_); }; @@ -189,11 +189,11 @@ Blockly.BlockDragSurfaceSvg.prototype.getGroup = function() { /** * Get the current blocks on the drag surface, if any (primarily * for BlockSvg.getRelativeToSurfaceXY). - * @return {!Element|undefined} Drag surface block DOM element, or undefined - * if no blocks exist. + * @return {Element} Drag surface block DOM element, or undefined if no blocks + * exist. */ Blockly.BlockDragSurfaceSvg.prototype.getCurrentBlock = function() { - return this.dragGroup_.firstChild; + return /** @type {Element} */ (this.dragGroup_.firstChild); }; /** diff --git a/core/block_dragger.js b/core/block_dragger.js index 7b8f3232b..fb1a78e28 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -26,6 +26,7 @@ goog.provide('Blockly.BlockDragger'); goog.require('Blockly.blockAnimations'); goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockMove'); +goog.require('Blockly.Events.Ui'); goog.require('Blockly.InsertionMarkerManager'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); @@ -100,14 +101,10 @@ Blockly.BlockDragger = function(block, workspace) { * @package */ Blockly.BlockDragger.prototype.dispose = function() { - this.draggingBlock_ = null; - this.workspace_ = null; - this.startWorkspace_ = null; this.dragIconData_.length = 0; if (this.draggedConnectionManager_) { this.draggedConnectionManager_.dispose(); - this.draggedConnectionManager_ = null; } }; @@ -123,7 +120,7 @@ Blockly.BlockDragger.initIconData_ = function(block) { // Build a list of icons that need to be moved and where they started. var dragIconData = []; var descendants = block.getDescendants(false); - for (var i = 0, descendant; descendant = descendants[i]; i++) { + for (var i = 0, descendant; (descendant = descendants[i]); i++) { var icons = descendant.getIcons(); for (var j = 0; j < icons.length; j++) { var data = { @@ -151,6 +148,7 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY, if (!Blockly.Events.getGroup()) { Blockly.Events.setGroup(true); } + this.fireDragStartEvent_(); // Mutators don't have the same type of z-ordering as the normal workspace // during a drag. They have to rely on the order of the blocks in the SVG. @@ -180,7 +178,7 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY, // For future consideration: we may be able to put moveToDragSurface inside // the block dragger, which would also let the block not track the block drag // surface. - this.draggingBlock_.moveToDragSurface_(); + this.draggingBlock_.moveToDragSurface(); var toolbox = this.workspace_.getToolbox(); if (toolbox) { @@ -190,6 +188,16 @@ Blockly.BlockDragger.prototype.startBlockDrag = function(currentDragDeltaXY, } }; +/** + * Fire a UI event at the start of a block drag. + * @private + */ +Blockly.BlockDragger.prototype.fireDragStartEvent_ = function() { + var event = new Blockly.Events.Ui(this.draggingBlock_, 'dragStart', + null, this.draggingBlock_.getDescendants(false)); + Blockly.Events.fire(event); +}; + /** * Execute a step of block dragging, based on the given event. Update the * display accordingly. @@ -222,19 +230,20 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { // Make sure internal state is fresh. this.dragBlock(e, currentDragDeltaXY); this.dragIconData_ = []; - + this.fireDragEndEvent_(); + Blockly.utils.dom.stopTextWidthCache(); Blockly.blockAnimations.disconnectUiStop(); var delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY); var newLoc = Blockly.utils.Coordinate.sum(this.startXY_, delta); - this.draggingBlock_.moveOffDragSurface_(newLoc); + this.draggingBlock_.moveOffDragSurface(newLoc); var deleted = this.maybeDeleteBlock_(); if (!deleted) { // These are expensive and don't need to be done if we're deleting. - this.draggingBlock_.moveConnections_(delta.x, delta.y); + this.draggingBlock_.moveConnections(delta.x, delta.y); this.draggingBlock_.setDragging(false); this.fireMoveEvent_(); if (this.draggedConnectionManager_.wouldConnectBlock()) { @@ -256,6 +265,16 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { Blockly.Events.setGroup(false); }; +/** + * Fire a UI event at the end of a block drag. + * @private + */ +Blockly.BlockDragger.prototype.fireDragEndEvent_ = function() { + var event = new Blockly.Events.Ui(this.draggingBlock_, 'dragStop', + this.draggingBlock_.getDescendants(false), null); + Blockly.Events.fire(event); +}; + /** * Fire a move event at the end of a block drag. * @private @@ -301,12 +320,12 @@ Blockly.BlockDragger.prototype.updateCursorDuringBlockDrag_ = function() { if (this.wouldDeleteBlock_) { this.draggingBlock_.setDeleteStyle(true); if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) { - trashcan.setOpen_(true); + trashcan.setOpen(true); } } else { this.draggingBlock_.setDeleteStyle(false); if (trashcan) { - trashcan.setOpen_(false); + trashcan.setOpen(false); } } }; diff --git a/core/block_events.js b/core/block_events.js index 7c80dfca7..beff260af 100644 --- a/core/block_events.js +++ b/core/block_events.js @@ -273,7 +273,7 @@ Blockly.Events.Create.prototype.run = function(forward) { xml.appendChild(this.xml); Blockly.Xml.domToWorkspace(xml, workspace); } else { - for (var i = 0, id; id = this.ids[i]; i++) { + for (var i = 0, id; (id = this.ids[i]); i++) { var block = workspace.getBlockById(id); if (block) { block.dispose(false); @@ -349,7 +349,7 @@ Blockly.Events.Delete.prototype.fromJson = function(json) { Blockly.Events.Delete.prototype.run = function(forward) { var workspace = this.getEventWorkspace_(); if (forward) { - for (var i = 0, id; id = this.ids[i]; i++) { + for (var i = 0, id; (id = this.ids[i]); i++) { var block = workspace.getBlockById(id); if (block) { block.dispose(false); diff --git a/core/block_svg.js b/core/block_svg.js index c34e05894..d42f5018c 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -23,6 +23,7 @@ goog.provide('Blockly.BlockSvg'); +goog.require('Blockly.ASTNode'); goog.require('Blockly.Block'); goog.require('Blockly.blockAnimations'); goog.require('Blockly.blockRendering.IPathObject'); @@ -31,7 +32,9 @@ goog.require('Blockly.Events'); goog.require('Blockly.Events.Ui'); goog.require('Blockly.Events.BlockMove'); goog.require('Blockly.Msg'); +goog.require('Blockly.navigation'); goog.require('Blockly.RenderedConnection'); +goog.require('Blockly.TabNavigateCursor'); goog.require('Blockly.Tooltip'); goog.require('Blockly.Touch'); goog.require('Blockly.utils'); @@ -39,7 +42,6 @@ goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Rect'); -goog.require('Blockly.Warning'); /** @@ -56,47 +58,39 @@ goog.require('Blockly.Warning'); Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { // Create core elements for the block. /** - * @type {SVGElement} + * @type {!SVGElement} * @private */ this.svgGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); this.svgGroup_.translate_ = ''; + /** + * A block style object. + * @type {!Blockly.Theme.BlockStyle} + */ + this.style = workspace.getRenderer().getConstants().getBlockStyle(null); + /** * The renderer's path object. * @type {Blockly.blockRendering.IPathObject} * @package */ - this.pathObject = - workspace.getRenderer().makePathObject(this.svgGroup_); - - // The next three paths are set only for backwards compatibility reasons. - /** - * The dark path of the block. - * @type {SVGElement} - * @private - */ - this.svgPathDark_ = this.pathObject.svgPathDark || null; - - /** - * The primary path of the block. - * @type {SVGElement} - * @private - */ - this.svgPath_ = this.pathObject.svgPath || null; - - /** - * The light path of the block. - * @type {SVGElement} - * @private - */ - this.svgPathLight_ = this.pathObject.svgPathLight || null; - - this.svgPath_.tooltip = this; + this.pathObject = workspace.getRenderer().makePathObject( + this.svgGroup_, this.style); /** @type {boolean} */ this.rendered = false; + /** @type {!Blockly.WorkspaceSvg} */ + this.workspace = workspace; + + /** @type {Blockly.RenderedConnection} */ + this.outputConnection = null; + /** @type {Blockly.RenderedConnection} */ + this.nextConnection = null; + /** @type {Blockly.RenderedConnection} */ + this.previousConnection = null; + /** * Whether to move the block to the drag surface when it is dragged. * True if it should move, false if it should be translated directly. @@ -104,9 +98,11 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { * @private */ this.useDragSurface_ = - Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_; + Blockly.utils.is3dSupported() && !!workspace.getBlockDragSurface(); - Blockly.Tooltip.bindMouseEvents(this.svgPath_); + var svgPath = this.pathObject.svgPath; + svgPath.tooltip = this; + Blockly.Tooltip.bindMouseEvents(svgPath); Blockly.BlockSvg.superClass_.constructor.call(this, workspace, prototypeName, opt_id); @@ -114,22 +110,6 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { if (this.svgGroup_.dataset) { this.svgGroup_.dataset['id'] = this.id; } - - /** - * 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} - * @private - */ - this.cursorSvg_ = null; - - /** - * 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} - * @private - */ - this.markerSvg_ = null; }; Blockly.utils.object.inherits(Blockly.BlockSvg, Blockly.Block); @@ -138,6 +118,7 @@ Blockly.utils.object.inherits(Blockly.BlockSvg, Blockly.Block); * Height is in workspace units. */ Blockly.BlockSvg.prototype.height = 0; + /** * Width of this block, including any connected value blocks. * Width is in workspace units. @@ -175,39 +156,6 @@ Blockly.BlockSvg.INLINE = -1; */ Blockly.BlockSvg.COLLAPSED_WARNING_ID = 'TEMP_COLLAPSED_WARNING_'; -// Leftover UI constants from block_render_svg.js. -/** - * Vertical space between elements. - * @const - * @package - */ -// TODO (#3142): Remove. -Blockly.BlockSvg.SEP_SPACE_Y = 10; - -/** - * Minimum height of a block. - * @const - * @package - */ -// TODO (#3142): Remove. -Blockly.BlockSvg.MIN_BLOCK_Y = 25; - -/** - * Width of horizontal puzzle tab. - * @const - * @package - */ -// TODO (#3142): Remove. -Blockly.BlockSvg.TAB_WIDTH = 8; - -/** - * Do blocks with no previous or output connections have a 'hat' on top? - * @const - * @package - */ -// TODO (#3142): Remove. -Blockly.BlockSvg.START_HAT = false; - /** * An optional method called when a mutator dialog is first opened. * This function must create and initialize a top-level block for the mutator @@ -228,6 +176,19 @@ Blockly.BlockSvg.prototype.decompose; */ Blockly.BlockSvg.prototype.compose; +/** + * An optional method for defining custom block context menu items. + * @type {?function(!Array.)} + */ +Blockly.BlockSvg.prototype.customContextMenu; + +/** + * An property used internally to reference the block's rendering debugger. + * @type {?Blockly.blockRendering.Debug} + * @package + */ +Blockly.BlockSvg.prototype.renderingDebugger; + /** * Create and initialize the SVG representation of the block. * May be called more than once. @@ -236,26 +197,70 @@ Blockly.BlockSvg.prototype.initSvg = function() { if (!this.workspace.rendered) { throw TypeError('Workspace is headless.'); } - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { input.init(); } var icons = this.getIcons(); for (var i = 0; i < icons.length; i++) { icons[i].createIcon(); } - this.updateColour(); - this.updateMovable(); - if (!this.workspace.options.readOnly && !this.eventsInit_) { + this.applyColour(); + this.pathObject.updateMovable(this.isMovable()); + var svg = this.getSvgRoot(); + if (!this.workspace.options.readOnly && !this.eventsInit_ && svg) { Blockly.bindEventWithChecks_( - this.getSvgRoot(), 'mousedown', this, this.onMouseDown_); + svg, 'mousedown', this, this.onMouseDown_); } this.eventsInit_ = true; - if (!this.getSvgRoot().parentNode) { - this.workspace.getCanvas().appendChild(this.getSvgRoot()); + if (!svg.parentNode) { + this.workspace.getCanvas().appendChild(svg); } }; +/** + * Get the secondary colour of a block. + * @return {?string} #RRGGBB string. + */ +Blockly.BlockSvg.prototype.getColourSecondary = function() { + return this.style.colourSecondary; +}; + +/** + * Get the tertiary colour of a block. + * @return {?string} #RRGGBB string. + */ +Blockly.BlockSvg.prototype.getColourTertiary = function() { + return this.style.colourTertiary; +}; + +/** + * Get the shadow colour of a block. + * @return {?string} #RRGGBB string. + * @deprecated Use style.colourSecondary. (2020 January 21) + */ +Blockly.BlockSvg.prototype.getColourShadow = function() { + return this.getColourSecondary(); +}; + +/** + * Get the border colour(s) of a block. + * @return {{colourDark, colourLight, colourBorder}} An object containing + * colour values for the border(s) of the block. If the block is using a + * style the colourBorder will be defined and equal to the tertiary colour + * of the style (#RRGGBB string). Otherwise the colourDark and colourLight + * attributes will be defined (#RRGGBB strings). + * @deprecated Use style.colourTertiary. (2020 January 21) + */ +Blockly.BlockSvg.prototype.getColourBorder = function() { + var colourTertiary = this.getColourTertiary(); + return { + colourBorder: colourTertiary, + colourLight: null, + colourDark: null + }; +}; + /** * Select this block. Highlight it visually. */ @@ -346,7 +351,7 @@ Blockly.BlockSvg.prototype.getIcons = function() { /** * Set parent of this block to be a new block or null. - * @param {Blockly.BlockSvg} newParent New parent block. + * @param {Blockly.Block} newParent New parent block. * @override */ Blockly.BlockSvg.prototype.setParent = function(newParent) { @@ -372,7 +377,7 @@ Blockly.BlockSvg.prototype.setParent = function(newParent) { newParent.getSvgRoot().appendChild(svgRoot); var newXY = this.getRelativeToSurfaceXY(); // Move the connections to match the child's new position. - this.moveConnections_(newXY.x - oldXY.x, newXY.y - oldXY.y); + this.moveConnections(newXY.x - oldXY.x, newXY.y - oldXY.y); } // If we are losing a parent, we want to move our DOM element to the // root of the workspace. @@ -380,6 +385,8 @@ Blockly.BlockSvg.prototype.setParent = function(newParent) { this.workspace.getCanvas().appendChild(svgRoot); this.translate(oldXY.x, oldXY.y); } + + this.applyColour(); }; /** @@ -396,7 +403,7 @@ Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() { var y = 0; var dragSurfaceGroup = this.useDragSurface_ ? - this.workspace.blockDragSurface_.getGroup() : null; + this.workspace.getBlockDragSurface().getGroup() : null; var element = this.getSvgRoot(); if (element) { @@ -408,13 +415,13 @@ Blockly.BlockSvg.prototype.getRelativeToSurfaceXY = function() { // If this element is the current element on the drag surface, include // the translation of the drag surface itself. if (this.useDragSurface_ && - this.workspace.blockDragSurface_.getCurrentBlock() == element) { + this.workspace.getBlockDragSurface().getCurrentBlock() == element) { var surfaceTranslation = - this.workspace.blockDragSurface_.getSurfaceTranslation(); + this.workspace.getBlockDragSurface().getSurfaceTranslation(); x += surfaceTranslation.x; y += surfaceTranslation.y; } - element = element.parentNode; + element = /** @type {!SVGElement} */ (element.parentNode); } while (element && element != this.workspace.getCanvas() && element != dragSurfaceGroup); } @@ -436,7 +443,7 @@ Blockly.BlockSvg.prototype.moveBy = function(dx, dy) { } var xy = this.getRelativeToSurfaceXY(); this.translate(xy.x + dx, xy.y + dy); - this.moveConnections_(dx, dy); + this.moveConnections(dx, dy); if (eventsEnabled) { event.recordNew(); Blockly.Events.fire(event); @@ -459,9 +466,9 @@ Blockly.BlockSvg.prototype.translate = function(x, y) { * Move this block to its workspace's drag surface, accounting for positioning. * Generally should be called at the same time as setDragging_(true). * Does nothing if useDragSurface_ is false. - * @private + * @package */ -Blockly.BlockSvg.prototype.moveToDragSurface_ = function() { +Blockly.BlockSvg.prototype.moveToDragSurface = function() { if (!this.useDragSurface_) { return; } @@ -471,9 +478,12 @@ Blockly.BlockSvg.prototype.moveToDragSurface_ = function() { // This is in workspace coordinates. var xy = this.getRelativeToSurfaceXY(); this.clearTransformAttributes_(); - this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y); + this.workspace.getBlockDragSurface().translateSurface(xy.x, xy.y); // Execute the move on the top-level SVG component - this.workspace.blockDragSurface_.setBlocksAndShow(this.getSvgRoot()); + var svg = this.getSvgRoot(); + if (svg) { + this.workspace.getBlockDragSurface().setBlocksAndShow(svg); + } }; /** @@ -491,15 +501,15 @@ Blockly.BlockSvg.prototype.moveTo = function(xy) { * Does nothing if useDragSurface_ is false. * @param {!Blockly.utils.Coordinate} newXY The position the block should take on * on the workspace canvas, in workspace coordinates. - * @private + * @package */ -Blockly.BlockSvg.prototype.moveOffDragSurface_ = function(newXY) { +Blockly.BlockSvg.prototype.moveOffDragSurface = function(newXY) { if (!this.useDragSurface_) { return; } // Translate to current position, turning off 3d. this.translate(newXY.x, newXY.y); - this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas()); + this.workspace.getBlockDragSurface().clearAndHide(this.workspace.getCanvas()); }; /** @@ -512,7 +522,7 @@ Blockly.BlockSvg.prototype.moveOffDragSurface_ = function(newXY) { */ Blockly.BlockSvg.prototype.moveDuringDrag = function(newLoc) { if (this.useDragSurface_) { - this.workspace.blockDragSurface_.translateSurface(newLoc.x, newLoc.y); + this.workspace.getBlockDragSurface().translateSurface(newLoc.x, newLoc.y); } else { this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')'; this.svgGroup_.setAttribute('transform', @@ -568,26 +578,18 @@ Blockly.BlockSvg.prototype.snapToGrid = function() { * @return {!Blockly.utils.Rect} Object with coordinates of the bounding box. */ Blockly.BlockSvg.prototype.getBoundingRectangle = function() { - var blockXY = this.getRelativeToSurfaceXY(this); - var tab = this.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0; + var blockXY = this.getRelativeToSurfaceXY(); var blockBounds = this.getHeightWidth(); - var top = blockXY.y; - var bottom = blockXY.y + blockBounds.height; var left, right; if (this.RTL) { - // Width has the tab built into it already so subtract it here. - left = blockXY.x - (blockBounds.width - tab); - // Add the width of the tab/puzzle piece knob to the x coordinate - // since X is the corner of the rectangle, not the whole puzzle piece. - right = blockXY.x + tab; + left = blockXY.x - blockBounds.width; + right = blockXY.x; } else { - // Subtract the width of the tab/puzzle piece knob to the x coordinate - // since X is the corner of the rectangle, not the whole puzzle piece. - left = blockXY.x - tab; - // Width has the tab built into it already so subtract it here. - right = blockXY.x + blockBounds.width - tab; + left = blockXY.x; + right = blockXY.x + blockBounds.width; } - return new Blockly.utils.Rect(top, bottom, left, right); + return new Blockly.utils.Rect( + blockXY.y, blockXY.y + blockBounds.height, left, right); }; /** @@ -595,7 +597,7 @@ Blockly.BlockSvg.prototype.getBoundingRectangle = function() { * A dirty field is a field that needs to be re-rendererd. */ Blockly.BlockSvg.prototype.markDirty = function() { - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { input.markDirty(); } }; @@ -610,7 +612,7 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { } var renderList = []; // Show/hide the inputs. - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { renderList.push.apply(renderList, input.setVisible(!collapsed)); } @@ -630,7 +632,7 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { var index = descendants.indexOf(nextBlock); descendants.splice(index, descendants.length - index); } - for (var i = 1, block; block = descendants[i]; i++) { + for (var i = 1, block; (block = descendants[i]); i++) { if (block.warning) { this.setWarningText(Blockly.Msg['COLLAPSED_WARNINGS_WARNING'], Blockly.BlockSvg.COLLAPSED_WARNING_ID); @@ -654,7 +656,7 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { renderList[0] = this; } if (this.rendered) { - for (var i = 0, block; block = renderList[i]; i++) { + for (var i = 0, block; (block = renderList[i]); i++) { block.render(); } // Don't bump neighbours. @@ -666,52 +668,23 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { /** * Open the next (or previous) FieldTextInput. - * @param {Blockly.Field|Blockly.Block} start Current location. + * @param {!Blockly.Field} start Current field. * @param {boolean} forward If true go forward, otherwise backward. */ Blockly.BlockSvg.prototype.tab = function(start, forward) { - var list = this.createTabList_(); - var i = list.indexOf(start); - if (i == -1) { - // No start location, start at the beginning or end. - i = forward ? -1 : list.length; - } - var target = list[forward ? i + 1 : i - 1]; - if (!target) { - // Ran off of list. - var parent = this.getParent(); - if (parent) { - parent.tab(this, forward); - } - } else if (target instanceof Blockly.Field) { - target.showEditor_(); - } else { - target.tab(null, forward); - } -}; + var tabCursor = new Blockly.TabNavigateCursor(); + tabCursor.setCurNode(Blockly.ASTNode.createFieldNode(start)); + var currentNode = tabCursor.getCurNode(); + var action = forward ? + Blockly.navigation.ACTION_NEXT : Blockly.navigation.ACTION_PREVIOUS; -/** - * Create an ordered list of all text fields and connected inputs. - * @return {!Array.} The ordered list. - * @private - */ -Blockly.BlockSvg.prototype.createTabList_ = function() { - // This function need not be efficient since it runs once on a keypress. - var list = []; - for (var i = 0, input; input = this.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { - if (field.isTabNavigable() && field.isVisible()) { - list.push(field); - } - } - if (input.connection) { - var block = input.connection.targetBlock(); - if (block) { - list.push(block); - } - } + tabCursor.onBlocklyAction(action); + + var nextNode = tabCursor.getCurNode(); + if (nextNode && nextNode !== currentNode) { + var nextField = /** @type {!Blockly.Field} */ (nextNode.getLocation()); + nextField.showEditor(); } - return list; }; /** @@ -728,9 +701,9 @@ Blockly.BlockSvg.prototype.onMouseDown_ = function(e) { /** * Load the block's help page in a new window. - * @private + * @package */ -Blockly.BlockSvg.prototype.showHelp_ = function() { +Blockly.BlockSvg.prototype.showHelp = function() { var url = (typeof this.helpUrl == 'function') ? this.helpUrl() : this.helpUrl; if (url) { window.open(url); @@ -839,9 +812,9 @@ Blockly.BlockSvg.prototype.generateContextMenu = function() { /** * Show the context menu for this block. * @param {!Event} e Mouse event. - * @private + * @package */ -Blockly.BlockSvg.prototype.showContextMenu_ = function(e) { +Blockly.BlockSvg.prototype.showContextMenu = function(e) { var menuOptions = this.generateContextMenu(); if (menuOptions && menuOptions.length) { @@ -857,9 +830,9 @@ Blockly.BlockSvg.prototype.showContextMenu_ = function(e) { * units. * @param {number} dy Vertical offset from current location, in workspace * units. - * @private + * @package */ -Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) { +Blockly.BlockSvg.prototype.moveConnections = function(dx, dy) { if (!this.rendered) { // Rendering is required to lay out the blocks. // This is probably an invisible block attached to a collapsed block. @@ -876,7 +849,7 @@ Blockly.BlockSvg.prototype.moveConnections_ = function(dx, dy) { // Recurse through all blocks attached under this one. for (var i = 0; i < this.childBlocks_.length; i++) { - this.childBlocks_[i].moveConnections_(dx, dy); + this.childBlocks_[i].moveConnections(dx, dy); } }; @@ -890,12 +863,12 @@ Blockly.BlockSvg.prototype.setDragging = function(adding) { var group = this.getSvgRoot(); group.translate_ = ''; group.skew_ = ''; - Blockly.draggingConnections_ = - Blockly.draggingConnections_.concat(this.getConnections_(true)); + Blockly.draggingConnections = + Blockly.draggingConnections.concat(this.getConnections_(true)); Blockly.utils.dom.addClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDragging'); } else { - Blockly.draggingConnections_ = []; + Blockly.draggingConnections = []; Blockly.utils.dom.removeClass( /** @type {!Element} */ (this.svgGroup_), 'blocklyDragging'); } @@ -905,26 +878,13 @@ Blockly.BlockSvg.prototype.setDragging = function(adding) { } }; -/** - * Add or remove the UI indicating if this block is movable or not. - */ -Blockly.BlockSvg.prototype.updateMovable = function() { - if (this.isMovable()) { - Blockly.utils.dom.addClass( - /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable'); - } else { - Blockly.utils.dom.removeClass( - /** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable'); - } -}; - /** * Set whether this block is movable or not. * @param {boolean} movable True if movable. */ Blockly.BlockSvg.prototype.setMovable = function(movable) { Blockly.BlockSvg.superClass_.setMovable.call(this, movable); - this.updateMovable(); + this.pathObject.updateMovable(movable); }; /** @@ -945,7 +905,7 @@ Blockly.BlockSvg.prototype.setEditable = function(editable) { */ Blockly.BlockSvg.prototype.setShadow = function(shadow) { Blockly.BlockSvg.superClass_.setShadow.call(this, shadow); - this.updateColour(); + this.applyColour(); }; /** @@ -961,14 +921,13 @@ Blockly.BlockSvg.prototype.setInsertionMarker = function(insertionMarker) { this.isInsertionMarker_ = insertionMarker; if (this.isInsertionMarker_) { this.setColour(Blockly.INSERTION_MARKER_COLOUR); - Blockly.utils.dom.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyInsertionMarker'); + this.pathObject.updateInsertionMarker(true); } }; /** * Return the root node of the SVG or null if none exists. - * @return {SVGElement} The root SVG node (probably a group). + * @return {!SVGElement} The root SVG node (probably a group). */ Blockly.BlockSvg.prototype.getSvgRoot = function() { return this.svgGroup_; @@ -980,6 +939,7 @@ Blockly.BlockSvg.prototype.getSvgRoot = function() { * the next statement with the previous statement. Otherwise, dispose of * all children of this block. * @param {boolean=} animate If true, show a disposal animation and sound. + * @suppress {checkTypes} */ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { if (!this.workspace) { @@ -1001,7 +961,7 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { Blockly.ContextMenu.hide(); } - if (Blockly.keyboardAccessibilityMode) { + if (this.workspace.keyboardAccessibilityMode) { Blockly.navigation.moveCursorOnBlockDelete(this); } @@ -1024,101 +984,41 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { for (var i = 0; i < icons.length; i++) { icons[i].dispose(); } - Blockly.BlockSvg.superClass_.dispose.call(this, healStack); + Blockly.BlockSvg.superClass_.dispose.call(this, !!healStack); Blockly.utils.dom.removeNode(this.svgGroup_); blockWorkspace.resizeContents(); // Sever JavaScript to DOM connections. this.svgGroup_ = null; - this.svgPath_ = null; - this.svgPathLight_ = null; - this.svgPathDark_ = null; Blockly.utils.dom.stopTextWidthCache(); }; /** * Change the colour of a block. + * @package */ -Blockly.BlockSvg.prototype.updateColour = function() { - if (!this.isEnabled()) { - // Disabled blocks don't have colour. - return; - } - - if (this.isShadow()) { - this.setShadowColour_(); - } else { - this.setBorderColour_(); - this.svgPath_.setAttribute('fill', this.getColour()); - } +Blockly.BlockSvg.prototype.applyColour = function() { + this.pathObject.applyColour(this); var icons = this.getIcons(); for (var i = 0; i < icons.length; i++) { - icons[i].updateColour(); + icons[i].applyColour(); } - for (var x = 0, input; input = this.inputList[x]; x++) { - for (var y = 0, field; field = input.fieldRow[y]; y++) { - field.updateColour(); + for (var x = 0, input; (input = this.inputList[x]); x++) { + for (var y = 0, field; (field = input.fieldRow[y]); y++) { + field.applyColour(); } } }; -/** - * Sets the colour of the border. - * Removes the light and dark paths if a border colour is defined. - */ -Blockly.BlockSvg.prototype.setBorderColour_ = function() { - var borderColours = this.getColourBorder(); - if (borderColours.colourBorder) { - this.svgPathLight_.style.display = 'none'; - this.svgPathDark_.style.display = 'none'; - - this.svgPath_.setAttribute('stroke', borderColours.colourBorder); - } else { - this.svgPathLight_.style.display = ''; - this.svgPathDark_.style.display = ''; - this.svgPath_.setAttribute('stroke', 'none'); - - this.svgPathLight_.setAttribute('stroke', borderColours.colourLight); - this.svgPathDark_.setAttribute('fill', borderColours.colourDark); - } -}; - -/** - * Sets the colour of shadow blocks. - * @return {?string} The background colour of the block. - */ -Blockly.BlockSvg.prototype.setShadowColour_ = function() { - var shadowColour = this.getColourShadow(); - - this.svgPathLight_.style.display = 'none'; - this.svgPathDark_.setAttribute('fill', shadowColour); - this.svgPath_.setAttribute('stroke', 'none'); - this.svgPath_.setAttribute('fill', shadowColour); - return shadowColour; -}; - /** * Enable or disable a block. */ Blockly.BlockSvg.prototype.updateDisabled = function() { - if (!this.isEnabled() || this.getInheritedDisabled()) { - var added = Blockly.utils.dom.addClass( - /** @type {!Element} */ (this.svgGroup_), 'blocklyDisabled'); - if (added) { - this.svgPath_.setAttribute('fill', - 'url(#' + this.workspace.options.disabledPatternId + ')'); - } - } else { - var removed = Blockly.utils.dom.removeClass( - /** @type {!Element} */ (this.svgGroup_), 'blocklyDisabled'); - if (removed) { - this.updateColour(); - } - } var children = this.getChildren(false); - for (var i = 0, child; child = children[i]; i++) { + this.applyColour(); + for (var i = 0, child; (child = children[i]); i++) { child.updateDisabled(); } }; @@ -1154,7 +1054,7 @@ Blockly.BlockSvg.prototype.setCommentText = function(text) { } if (shouldHaveComment) { this.commentIcon_ = new Blockly.Comment(this); - this.comment = this.commentIcon_; // For backwards compatibility. + this.comment = this.commentIcon_; // For backwards compatibility. } else { this.commentIcon_.dispose(); this.commentIcon_ = null; @@ -1174,6 +1074,9 @@ Blockly.BlockSvg.prototype.setCommentText = function(text) { * maintain multiple warnings. */ Blockly.BlockSvg.prototype.setWarningText = function(text, opt_id) { + if (!Blockly.Warning) { + throw Error('Missing require for Blockly.Warning'); + } if (!this.warningTextDb_) { // Create a database of warning PIDs. // Only runs once per block (and only those with warnings). @@ -1259,10 +1162,15 @@ Blockly.BlockSvg.prototype.setMutator = function(mutator) { this.mutator.dispose(); } if (mutator) { - mutator.block_ = this; + mutator.setBlock(this); this.mutator = mutator; mutator.createIcon(); } + if (this.rendered) { + this.render(); + // Adding or removing a mutator icon will cause the block to change shape. + this.bumpNeighbours(); + } }; /** @@ -1298,30 +1206,21 @@ Blockly.BlockSvg.prototype.setHighlighted = function(highlighted) { if (!this.rendered) { return; } - if (highlighted) { - this.svgPath_.setAttribute('filter', - 'url(#' + this.workspace.options.embossFilterId + ')'); - this.svgPathLight_.style.display = 'none'; - } else { - this.svgPath_.setAttribute('filter', 'none'); - this.svgPathLight_.style.display = 'inline'; - } + this.pathObject.updateHighlighted(highlighted); }; /** * Select this block. Highlight it visually. */ Blockly.BlockSvg.prototype.addSelect = function() { - Blockly.utils.dom.addClass( - /** @type {!Element} */ (this.svgGroup_), 'blocklySelected'); + this.pathObject.updateSelected(true); }; /** * Unselect this block. Remove its highlighting. */ Blockly.BlockSvg.prototype.removeSelect = function() { - Blockly.utils.dom.removeClass( - /** @type {!Element} */ (this.svgGroup_), 'blocklySelected'); + this.pathObject.updateSelected(false); }; /** @@ -1331,17 +1230,19 @@ Blockly.BlockSvg.prototype.removeSelect = function() { * @package */ Blockly.BlockSvg.prototype.setDeleteStyle = function(enable) { - if (enable) { - Blockly.utils.dom.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDraggingDelete'); - } else { - Blockly.utils.dom.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyDraggingDelete'); - } + this.pathObject.updateDraggingDelete(enable); }; + // Overrides of functions on Blockly.Block that take into account whether the // block has been rendered. +/** + * Get the colour of a block. + * @return {string} #RRGGBB string. + */ +Blockly.BlockSvg.prototype.getColour = function() { + return this.style.colourPrimary; +}; /** * Change the colour of a block. @@ -1349,9 +1250,36 @@ Blockly.BlockSvg.prototype.setDeleteStyle = function(enable) { */ Blockly.BlockSvg.prototype.setColour = function(colour) { Blockly.BlockSvg.superClass_.setColour.call(this, colour); + var styleObj = this.workspace.getRenderer().getConstants() + .getBlockStyleForColour(this.colour_); - if (this.rendered) { - this.updateColour(); + this.pathObject.setStyle(styleObj.style); + this.style = styleObj.style; + this.styleName_ = styleObj.name; + + this.applyColour(); +}; + +/** + * Set the style and colour values of a block. + * @param {string} blockStyleName Name of the block style + * @throws {Error} if the block style does not exist. + */ +Blockly.BlockSvg.prototype.setStyle = function(blockStyleName) { + var blockStyle = this.workspace.getRenderer() + .getConstants().getBlockStyle(blockStyleName); + this.styleName_ = blockStyleName; + + if (blockStyle) { + this.hat = blockStyle.hat; + this.pathObject.setStyle(blockStyle); + // Set colour to match Block. + this.colour_ = blockStyle.colourPrimary; + this.style = blockStyle; + + this.applyColour(); + } else { + throw Error('Invalid style name: ' + blockStyleName); } }; @@ -1366,7 +1294,12 @@ Blockly.BlockSvg.prototype.bringToFront = function() { var block = this; do { var root = block.getSvgRoot(); - root.parentNode.appendChild(root); + var parent = root.parentNode; + var childNodes = parent.childNodes; + // Avoid moving the block if it's already at the bottom. + if (childNodes[childNodes.length - 1] !== root) { + parent.appendChild(root); + } block = block.getParent(); } while (block); }; @@ -1488,35 +1421,45 @@ Blockly.BlockSvg.prototype.appendInput_ = function(type, name) { }; /** - * Set whether the connections are hidden (not tracked in a database) or not. - * Recursively walk down all child blocks (except collapsed blocks). - * @param {boolean} hidden True if connections are hidden. + * Sets whether this block's connections are tracked in the database or not. + * + * Used by the deserializer to be more efficient. Setting a connection's + * tracked_ value to false keeps it from adding itself to the db when it + * gets its first moveTo call, saving expensive ops for later. + * @param {boolean} track If true, start tracking. If false, stop tracking. * @package */ -Blockly.BlockSvg.prototype.setConnectionsHidden = function(hidden) { - if (!hidden && this.isCollapsed()) { - if (this.outputConnection) { - this.outputConnection.setHidden(hidden); +Blockly.BlockSvg.prototype.setConnectionTracking = function(track) { + if (this.previousConnection) { + this.previousConnection.setTracking(track); + } + if (this.outputConnection) { + this.outputConnection.setTracking(track); + } + if (this.nextConnection) { + this.nextConnection.setTracking(track); + var child = this.nextConnection.targetBlock(); + if (child) { + child.setConnectionTracking(track); } - if (this.previousConnection) { - this.previousConnection.setHidden(hidden); - } - if (this.nextConnection) { - this.nextConnection.setHidden(hidden); - var child = this.nextConnection.targetBlock(); - if (child) { - child.setConnectionsHidden(hidden); - } - } - } else { - var myConnections = this.getConnections_(true); - for (var i = 0, connection; connection = myConnections[i]; i++) { - connection.setHidden(hidden); - if (connection.isSuperior()) { - var child = connection.targetBlock(); - if (child) { - child.setConnectionsHidden(hidden); - } + } + + if (this.collapsed_) { + // When track is true, we don't want to start tracking collapsed + // connections. When track is false, we're already not tracking + // collapsed connections, so no need to update. + return; + } + + for (var i = 0; i < this.inputList.length; i++) { + var conn = this.inputList[i].connection; + if (conn) { + conn.setTracking(track); + + // Pass tracking on down the chain. + var block = conn.targetBlock(); + if (block) { + block.setConnectionTracking(track); } } } @@ -1527,7 +1470,7 @@ Blockly.BlockSvg.prototype.setConnectionsHidden = function(hidden) { * @param {boolean} all If true, return all connections even hidden ones. * Otherwise, for a non-rendered block return an empty list, and for a * collapsed block don't return inputs connections. - * @return {!Array.} Array of connections. + * @return {!Array.} Array of connections. * @package */ Blockly.BlockSvg.prototype.getConnections_ = function(all) { @@ -1543,7 +1486,7 @@ Blockly.BlockSvg.prototype.getConnections_ = function(all) { myConnections.push(this.nextConnection); } if (all || !this.collapsed_) { - for (var i = 0, input; input = this.inputList[i]; i++) { + for (var i = 0, input; (input = this.inputList[i]); i++) { if (input.connection) { myConnections.push(input.connection); } @@ -1553,11 +1496,40 @@ Blockly.BlockSvg.prototype.getConnections_ = function(all) { return myConnections; }; +/** + * Walks down a stack of blocks and finds the last next connection on the stack. + * @return {Blockly.RenderedConnection} The last next connection on the stack, + * or null. + * @package + * @override + */ +Blockly.BlockSvg.prototype.lastConnectionInStack = function() { + return /** @type {Blockly.RenderedConnection} */ ( + Blockly.BlockSvg.superClass_.lastConnectionInStack.call(this)); +}; + +/** + * Find the connection on this block that corresponds to the given connection + * on the other block. + * Used to match connections between a block and its insertion marker. + * @param {!Blockly.Block} otherBlock The other block to match against. + * @param {!Blockly.Connection} conn The other connection to match. + * @return {Blockly.RenderedConnection} The matching connection on this block, + * or null. + * @package + * @override + */ +Blockly.BlockSvg.prototype.getMatchingConnection = function(otherBlock, conn) { + return /** @type {Blockly.RenderedConnection} */ ( + Blockly.BlockSvg.superClass_.getMatchingConnection.call(this, + otherBlock, conn)); +}; + /** * Create a connection of the specified type. * @param {number} type The type of the connection to create. * @return {!Blockly.RenderedConnection} A new connection of the specified type. - * @private + * @protected */ Blockly.BlockSvg.prototype.makeConnection_ = function(type) { return new Blockly.RenderedConnection(this, type); @@ -1580,15 +1552,15 @@ Blockly.BlockSvg.prototype.bumpNeighbours = function() { } // Loop through every connection on this block. var myConnections = this.getConnections_(false); - for (var i = 0, connection; connection = myConnections[i]; i++) { + for (var i = 0, connection; (connection = myConnections[i]); i++) { // Spider down from this block bumping all sub-blocks. if (connection.isConnected() && connection.isSuperior()) { connection.targetBlock().bumpNeighbours(); } - var neighbours = connection.neighbours_(Blockly.SNAP_RADIUS); - for (var j = 0, otherConnection; otherConnection = neighbours[j]; j++) { + var neighbours = connection.neighbours(Blockly.SNAP_RADIUS); + for (var j = 0, otherConnection; (otherConnection = neighbours[j]); j++) { // If both connections are connected, that's probably fine. But if // either one of them is unconnected, then there could be confusion. @@ -1598,9 +1570,9 @@ Blockly.BlockSvg.prototype.bumpNeighbours = function() { // Always bump the inferior block. if (connection.isSuperior()) { - otherConnection.bumpAwayFrom_(connection); + otherConnection.bumpAwayFrom(connection); } else { - connection.bumpAwayFrom_(otherConnection); + connection.bumpAwayFrom(otherConnection); } } } @@ -1635,10 +1607,11 @@ Blockly.BlockSvg.prototype.scheduleSnapAndBump = function() { * Position a block so that it doesn't move the target block when connected. * The block to position is usually either the first block in a dragged stack or * an insertion marker. - * @param {!Blockly.Connection} sourceConnection The connection on the moving - * block's stack. - * @param {!Blockly.Connection} targetConnection The connection that should stay - * stationary as this block is positioned. + * @param {!Blockly.RenderedConnection} sourceConnection The connection on the + * moving block's stack. + * @param {!Blockly.RenderedConnection} targetConnection The connection that + * should stay stationary as this block is positioned. + * @package */ Blockly.BlockSvg.prototype.positionNearConnection = function(sourceConnection, targetConnection) { @@ -1646,13 +1619,34 @@ Blockly.BlockSvg.prototype.positionNearConnection = function(sourceConnection, // otherwise its position is set by the previous block. if (sourceConnection.type == Blockly.NEXT_STATEMENT || sourceConnection.type == Blockly.INPUT_VALUE) { - var dx = targetConnection.x_ - sourceConnection.x_; - var dy = targetConnection.y_ - sourceConnection.y_; + var dx = targetConnection.x - sourceConnection.x; + var dy = targetConnection.y - sourceConnection.y; this.moveBy(dx, dy); } }; +/** + * Return the parent block or null if this block is at the top level. + * @return {Blockly.BlockSvg} The block that holds the current block. + * @override + */ +Blockly.BlockSvg.prototype.getParent = function() { + return /** @type {!Blockly.BlockSvg} */ ( + Blockly.BlockSvg.superClass_.getParent.call(this)); +}; + +/** + * Return the top-most block in this block's tree. + * This will return itself if this block is at the top level. + * @return {!Blockly.BlockSvg} The root block. + * @override + */ +Blockly.BlockSvg.prototype.getRootBlock = function() { + return /** @type {!Blockly.BlockSvg} */ ( + Blockly.BlockSvg.superClass_.getRootBlock.call(this)); +}; + /** * Render the block. * Lays out and reflows a block based on its contents and settings. @@ -1676,6 +1670,11 @@ Blockly.BlockSvg.prototype.render = function(opt_bubble) { } } Blockly.utils.dom.stopTextWidthCache(); + + var cursor = this.workspace.getCursor(); + if (this.pathObject.cursorSvg_) { + cursor.draw(); + } }; /** @@ -1700,7 +1699,7 @@ Blockly.BlockSvg.prototype.updateConnectionLocations_ = function() { if (conn) { conn.moveToOffset(blockTL); if (conn.isConnected()) { - conn.tighten_(); + conn.tighten(); } } } @@ -1708,7 +1707,7 @@ Blockly.BlockSvg.prototype.updateConnectionLocations_ = function() { if (this.nextConnection) { this.nextConnection.moveToOffset(blockTL); if (this.nextConnection.isConnected()) { - this.nextConnection.tighten_(); + this.nextConnection.tighten(); } } }; @@ -1720,13 +1719,7 @@ Blockly.BlockSvg.prototype.updateConnectionLocations_ = function() { * @package */ Blockly.BlockSvg.prototype.setCursorSvg = function(cursorSvg) { - if (!cursorSvg) { - this.cursorSvg_ = null; - return; - } - - this.svgGroup_.appendChild(cursorSvg); - this.cursorSvg_ = cursorSvg; + this.pathObject.setCursorSvg(cursorSvg); }; /** @@ -1736,17 +1729,7 @@ Blockly.BlockSvg.prototype.setCursorSvg = function(cursorSvg) { * @package */ Blockly.BlockSvg.prototype.setMarkerSvg = function(markerSvg) { - if (!markerSvg) { - this.markerSvg_ = null; - return; - } - - if (this.cursorSvg_) { - this.svgGroup_.insertBefore(markerSvg, this.cursorSvg_); - } else { - this.svgGroup_.appendChild(markerSvg); - } - this.markerSvg_ = markerSvg; + this.pathObject.setMarkerSvg(markerSvg); }; /** @@ -1763,38 +1746,14 @@ Blockly.BlockSvg.prototype.getHeightWidth = function() { var nextBlock = this.getNextBlock(); if (nextBlock) { var nextHeightWidth = nextBlock.getHeightWidth(); - height += nextHeightWidth.height - 4; // Height of tab. + var workspace = /** @type {!Blockly.WorkspaceSvg} */ (this.workspace); + var tabHeight = workspace.getRenderer().getConstants().NOTCH_HEIGHT; + height += nextHeightWidth.height - tabHeight; width = Math.max(width, nextHeightWidth.width); } return {height: height, width: width}; }; -/** - * Position a new block correctly, so that it doesn't move the existing block - * when connected to it. - * @param {!Blockly.Block} newBlock The block to position - either the first - * block in a dragged stack or an insertion marker. - * @param {!Blockly.Connection} newConnection The connection on the new block's - * stack - either a connection on newBlock, or the last NEXT_STATEMENT - * connection on the stack if the stack's being dropped before another - * block. - * @param {!Blockly.Connection} existingConnection The connection on the - * existing block, which newBlock should line up with. - * @package - */ -Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, - existingConnection) { - // We only need to position the new block if it's before the existing one, - // otherwise its position is set by the previous block. - if (newConnection.type == Blockly.NEXT_STATEMENT || - newConnection.type == Blockly.INPUT_VALUE) { - var dx = existingConnection.x_ - newConnection.x_; - var dy = existingConnection.y_ - newConnection.y_; - - newBlock.moveBy(dx, dy); - } -}; - /** * Visual effect to show that if the dragging block is dropped, this block will * be replaced. If a shadow block, it will disappear. Otherwise it will bump. @@ -1802,11 +1761,16 @@ Blockly.BlockSvg.prototype.positionNewBlock = function(newBlock, newConnection, * @package */ Blockly.BlockSvg.prototype.highlightForReplacement = function(add) { - if (add) { - Blockly.utils.dom.addClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyReplaceable'); - } else { - Blockly.utils.dom.removeClass(/** @type {!Element} */ (this.svgGroup_), - 'blocklyReplaceable'); - } + this.pathObject.updateReplacementHighlight(add); +}; + +/** + * Visual effect to show that if the dragging block is dropped it will connect + * to this input. + * @param {Blockly.Connection} conn The connection on the input to highlight. + * @param {boolean} add True if highlighting should be added. + * @package + */ +Blockly.BlockSvg.prototype.highlightShapeForInput = function(conn, add) { + this.pathObject.updateShapeForInputHighlight(conn, add); }; diff --git a/core/blockly.js b/core/blockly.js index 6b5abf6fa..63c9578fa 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -66,24 +66,12 @@ Blockly.mainWorkspace = null; */ Blockly.selected = null; -/** - * Current cursor. - * @type {Blockly.Cursor} - */ -Blockly.cursor = null; - -/** - * Whether or not we're currently in keyboard accessibility mode. - * @type {boolean} - */ -Blockly.keyboardAccessibilityMode = false; - /** * All of the connections on blocks that are currently being dragged. * @type {!Array.} - * @private + * @package */ -Blockly.draggingConnections_ = []; +Blockly.draggingConnections = []; /** * Contents of the local clipboard. @@ -113,9 +101,16 @@ Blockly.clipboardTypeCounts_ = null; */ Blockly.cache3dSupported_ = null; +/** + * Blockly opaque event data used to unbind events when using + * `Blockly.bindEvent_` and `Blockly.bindEventWithChecks_`. + * @typedef {!Array.} + */ +Blockly.EventData; + /** * Returns the dimensions of the specified SVG image. - * @param {!Element} svg SVG image. + * @param {!SVGElement} svg SVG image. * @return {!Object} Contains width and height properties. */ Blockly.svgSize = function(svg) { @@ -170,12 +165,15 @@ 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. - * @private + * @package */ // TODO (https://github.com/google/blockly/issues/1998) handle cases where there // are multiple workspaces and non-main workspaces are able to accept input. -Blockly.onKeyDown_ = function(e) { +Blockly.onKeyDown = function(e) { var mainWorkspace = Blockly.mainWorkspace; + if (!mainWorkspace) { + return; + } if (Blockly.utils.isTargetInput(e) || (mainWorkspace.rendered && !mainWorkspace.isVisible())) { @@ -262,7 +260,8 @@ Blockly.onKeyDown_ = function(e) { if (deleteBlock && !Blockly.selected.workspace.isFlyout) { Blockly.Events.setGroup(true); Blockly.hideChaff(); - Blockly.selected.dispose(/* heal */ true, true); + var selected = /** @type {!Blockly.BlockSvg} */ (Blockly.selected); + selected.dispose(/* heal */ true, true); Blockly.Events.setGroup(false); } }; @@ -295,9 +294,9 @@ Blockly.copy_ = function(toCopy) { * Duplicate this block and its children, or a workspace comment. * @param {!Blockly.Block | !Blockly.WorkspaceComment} toDuplicate Block or * Workspace Comment to be copied. - * @private + * @package */ -Blockly.duplicate_ = function(toDuplicate) { +Blockly.duplicate = function(toDuplicate) { // Save the clipboard. var clipboardXml = Blockly.clipboardXml_; var clipboardSource = Blockly.clipboardSource_; @@ -336,13 +335,14 @@ Blockly.hideChaff = function(opt_allowToolbox) { // For now the trashcan flyout always autocloses because it overlays the // trashcan UI (no trashcan to click to close it). if (workspace.trashcan && - workspace.trashcan.flyout_) { - workspace.trashcan.flyout_.hide(); + workspace.trashcan.flyout) { + workspace.trashcan.flyout.hide(); } - if (workspace.toolbox_ && - workspace.toolbox_.flyout_ && - workspace.toolbox_.flyout_.autoClose) { - workspace.toolbox_.clearSelection(); + var toolbox = workspace.getToolbox(); + if (toolbox && + toolbox.getFlyout() && + toolbox.getFlyout().autoClose) { + toolbox.clearSelection(); } } }; @@ -354,7 +354,7 @@ Blockly.hideChaff = function(opt_allowToolbox) { * @return {!Blockly.Workspace} The main workspace. */ Blockly.getMainWorkspace = function() { - return Blockly.mainWorkspace; + return /** @type {!Blockly.Workspace} */ (Blockly.mainWorkspace); }; /** @@ -387,7 +387,7 @@ Blockly.confirm = function(message, callback) { * recommend testing mobile when overriding this. * @param {string} message The message to display to the user. * @param {string} defaultValue The value to initialize the prompt with. - * @param {!function(string)} callback The callback for handling user response. + * @param {!function(?string)} callback The callback for handling user response. */ Blockly.prompt = function(message, defaultValue, callback) { callback(prompt(message, defaultValue)); @@ -454,7 +454,7 @@ Blockly.defineBlocksWithJsonArray = function(jsonArray) { * should prevent the default handler. False by default. If * opt_noPreventDefault is provided, opt_noCaptureIdentifier must also be * provided. - * @return {!Array.} Opaque data that can be passed to unbindEvent_. + * @return {!Blockly.EventData} Opaque data that can be passed to unbindEvent_. */ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, opt_noCaptureIdentifier, opt_noPreventDefault) { @@ -464,7 +464,7 @@ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, // Handle each touch point separately. If the event was a mouse event, this // will hand back an array with one element, which we're fine handling. var events = Blockly.Touch.splitEventByTouches(e); - for (var i = 0, event; event = events[i]; i++) { + for (var i = 0, event; (event = events[i]); i++) { if (captureIdentifier && !Blockly.Touch.shouldHandleEvent(event)) { continue; } @@ -481,7 +481,7 @@ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, var bindData = []; if (Blockly.utils.global['PointerEvent'] && (name in Blockly.Touch.TOUCH_MAP)) { - for (var i = 0, type; type = Blockly.Touch.TOUCH_MAP[name][i]; i++) { + for (var i = 0, type; (type = Blockly.Touch.TOUCH_MAP[name][i]); i++) { node.addEventListener(type, wrapFunc, false); bindData.push([node, type, wrapFunc]); } @@ -500,7 +500,7 @@ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, e.preventDefault(); } }; - for (var i = 0, type; type = Blockly.Touch.TOUCH_MAP[name][i]; i++) { + for (var i = 0, type; (type = Blockly.Touch.TOUCH_MAP[name][i]); i++) { node.addEventListener(type, touchWrapFunc, false); bindData.push([node, type, touchWrapFunc]); } @@ -513,14 +513,13 @@ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, /** * Bind an event to a function call. Handles multitouch events by using the * coordinates of the first changed touch, and doesn't do any safety checks for - * simultaneous event processing. - * @deprecated in favor of bindEventWithChecks_, but preserved for external - * users. + * simultaneous event processing. In most cases prefer is to use + * `Blockly.bindEventWithChecks_`. * @param {!EventTarget} node Node upon which to listen. * @param {string} name Event name to listen to (e.g. 'mousedown'). * @param {Object} thisObject The value of 'this' in the function. * @param {!Function} func Function to call when event is triggered. - * @return {!Array.} Opaque data that can be passed to unbindEvent_. + * @return {!Blockly.EventData} Opaque data that can be passed to unbindEvent_. */ Blockly.bindEvent_ = function(node, name, thisObject, func) { var wrapFunc = function(e) { @@ -534,7 +533,7 @@ Blockly.bindEvent_ = function(node, name, thisObject, func) { var bindData = []; if (Blockly.utils.global['PointerEvent'] && (name in Blockly.Touch.TOUCH_MAP)) { - for (var i = 0, type; type = Blockly.Touch.TOUCH_MAP[name][i]; i++) { + for (var i = 0, type; (type = Blockly.Touch.TOUCH_MAP[name][i]); i++) { node.addEventListener(type, wrapFunc, false); bindData.push([node, type, wrapFunc]); } @@ -557,7 +556,7 @@ Blockly.bindEvent_ = function(node, name, thisObject, func) { // Stop the browser from scrolling/zooming the page. e.preventDefault(); }; - for (var i = 0, type; type = Blockly.Touch.TOUCH_MAP[name][i]; i++) { + for (var i = 0, type; (type = Blockly.Touch.TOUCH_MAP[name][i]); i++) { node.addEventListener(type, touchWrapFunc, false); bindData.push([node, type, touchWrapFunc]); } diff --git a/core/blocks.js b/core/blocks.js index 328eacf43..ca1367afe 100644 --- a/core/blocks.js +++ b/core/blocks.js @@ -27,7 +27,7 @@ */ goog.provide('Blockly.Blocks'); -/* +/** * A mapping of block type names to block prototype objects. * @type {!Object.} */ diff --git a/core/bubble.js b/core/bubble.js index 8c245631b..f66982b07 100644 --- a/core/bubble.js +++ b/core/bubble.js @@ -51,6 +51,42 @@ Blockly.Bubble = function(workspace, content, shape, anchorXY, this.content_ = content; this.shape_ = shape; + /** + * Method to call on resize of bubble. + * @type {?function()} + * @private + */ + this.resizeCallback_ = null; + + /** + * Method to call on move of bubble. + * @type {?function()} + * @private + */ + this.moveCallback_ = null; + + /** + * Mouse down on bubbleBack_ event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseDownBubbleWrapper_ = null; + + /** + * Mouse down on resizeGroup_ event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseDownResizeWrapper_ = null; + + /** + * Describes whether this bubble has been disposed of (nodes and event + * listeners removed from the page) or not. + * @type {boolean} + * @package + */ + this.disposed = false; + var angle = Blockly.Bubble.ARROW_ANGLE; if (this.workspace_.RTL) { angle = -angle; @@ -72,15 +108,6 @@ Blockly.Bubble = function(workspace, content, shape, anchorXY, this.positionBubble_(); this.renderArrow_(); this.rendered_ = true; - - if (!workspace.options.readOnly) { - Blockly.bindEventWithChecks_( - this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_); - if (this.resizeGroup_) { - Blockly.bindEventWithChecks_( - this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_); - } - } }; /** @@ -110,25 +137,19 @@ Blockly.Bubble.ARROW_BEND = 4; Blockly.Bubble.ANCHOR_RADIUS = 8; /** - * Wrapper function called when a mouseUp occurs during a drag operation. - * @type {Array.} + * Mouse up event data. + * @type {?Blockly.EventData} * @private */ Blockly.Bubble.onMouseUpWrapper_ = null; /** - * Wrapper function called when a mouseMove occurs during a drag operation. - * @type {Array.} + * Mouse move event data. + * @type {?Blockly.EventData} * @private */ Blockly.Bubble.onMouseMoveWrapper_ = null; -/** - * Function to call on resize of bubble. - * @type {Function} - */ -Blockly.Bubble.prototype.resizeCallback_ = null; - /** * Stop binding to the global mouseup and mousemove events. * @private @@ -144,12 +165,12 @@ Blockly.Bubble.unbindDragEvents_ = function() { } }; -/* +/** * Handle a mouse-up event while dragging a bubble's border or resize handle. - * @param {!Event} e Mouse up event. + * @param {!Event} _e Mouse up event. * @private */ -Blockly.Bubble.bubbleMouseUp_ = function(/* e */) { +Blockly.Bubble.bubbleMouseUp_ = function(_e) { Blockly.Touch.clearTouchIdentifier(); Blockly.Bubble.unbindDragEvents_(); }; @@ -224,7 +245,8 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { */ this.bubbleGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); var filter = - {'filter': 'url(#' + this.workspace_.options.embossFilterId + ')'}; + {'filter': 'url(#' + + this.workspace_.getRenderer().getConstants().embossFilterId + ')'}; if (Blockly.utils.userAgent.JAVA_FX) { // Multiple reports that JavaFX can't handle filters. // https://github.com/google/blockly/issues/99 @@ -232,7 +254,8 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { } var bubbleEmboss = Blockly.utils.dom.createSvgElement('g', filter, this.bubbleGroup_); - this.bubbleArrow_ = Blockly.utils.dom.createSvgElement('path', {}, bubbleEmboss); + this.bubbleArrow_ = Blockly.utils.dom.createSvgElement('path', {}, + bubbleEmboss); this.bubbleBack_ = Blockly.utils.dom.createSvgElement('rect', { 'class': 'blocklyDraggable', @@ -268,6 +291,15 @@ Blockly.Bubble.prototype.createDom_ = function(content, hasResize) { } else { this.resizeGroup_ = null; } + + if (!this.workspace_.options.readOnly) { + this.onMouseDownBubbleWrapper_ = Blockly.bindEventWithChecks_( + this.bubbleBack_, 'mousedown', this, this.bubbleMouseDown_); + if (this.resizeGroup_) { + this.onMouseDownResizeWrapper_ = Blockly.bindEventWithChecks_( + this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_); + } + } this.bubbleGroup_.appendChild(content); return this.bubbleGroup_; }; @@ -305,9 +337,9 @@ Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) { /** * Show the context menu for this bubble. * @param {!Event} _e Mouse event. - * @private + * @package */ -Blockly.Bubble.prototype.showContextMenu_ = function(_e) { +Blockly.Bubble.prototype.showContextMenu = function(_e) { // NOP on bubbles, but used by the bubble dragger to pass events to // workspace comments. }; @@ -327,7 +359,7 @@ Blockly.Bubble.prototype.isDeletable = function() { * @private */ Blockly.Bubble.prototype.resizeMouseDown_ = function(e) { - this.promote_(); + this.promote(); Blockly.Bubble.unbindDragEvents_(); if (Blockly.utils.isRightButton(e)) { // No right-click. @@ -370,12 +402,20 @@ Blockly.Bubble.prototype.registerResizeEvent = function(callback) { this.resizeCallback_ = callback; }; +/** + * Register a function as a callback event for when the bubble is moved. + * @param {!Function} callback The function to call on move. + */ +Blockly.Bubble.prototype.registerMoveEvent = function(callback) { + this.moveCallback_ = callback; +}; + /** * Move this bubble to the top of the stack. * @return {boolean} Whether or not the bubble has been moved. - * @private + * @package */ -Blockly.Bubble.prototype.promote_ = function() { +Blockly.Bubble.prototype.promote = function() { var svgGroup = this.bubbleGroup_.parentNode; if (svgGroup.lastChild !== this.bubbleGroup_) { svgGroup.appendChild(this.bubbleGroup_); @@ -412,8 +452,11 @@ Blockly.Bubble.prototype.layoutBubble_ = function() { var optimalTop = this.getOptimalRelativeTop_(metrics); var bbox = this.shape_.getBBox(); - var topPosition = {x: optimalLeft, - y: -this.height_ - Blockly.BlockSvg.MIN_BLOCK_Y}; + var topPosition = { + x: optimalLeft, + y: -this.height_ - + this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT + }; var startPosition = {x: -this.width_ - 30, y: optimalTop}; var endPosition = {x: bbox.width, y: optimalTop}; var bottomPosition = {x: optimalLeft, y: bbox.height}; @@ -455,12 +498,10 @@ Blockly.Bubble.prototype.layoutBubble_ = function() { /** * Calculate the what percentage of the bubble overlaps with the visible * workspace (what percentage of the bubble is visible). - * @param {!Object} relativeMin The position of the top-left corner of the - * bubble relative to the anchor point. - * @param {number} relativeMin.x The x-position of the relativeMin. - * @param {number} relativeMin.y The y-position of the relativeMin. + * @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. + * appear in. * @return {number} The percentage of the bubble that is visible. * @private */ @@ -621,6 +662,17 @@ Blockly.Bubble.prototype.moveTo = function(x, y) { this.bubbleGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')'); }; +/** + * Triggers a move callback if one exists at the end of a drag. + * @param {boolean} adding True if adding, false if removing. + * @package + */ +Blockly.Bubble.prototype.setDragging = function(adding) { + if (!adding && this.moveCallback_) { + this.moveCallback_(); + } +}; + /** * Get the dimensions of this bubble. * @return {!Blockly.utils.Size} The height and width of the bubble. @@ -755,16 +807,15 @@ Blockly.Bubble.prototype.setColour = function(hexColour) { * Dispose of this bubble. */ Blockly.Bubble.prototype.dispose = function() { + if (this.onMouseDownBubbleWrapper_) { + Blockly.unbindEvent_(this.onMouseDownBubbleWrapper_); + } + if (this.onMouseDownResizeWrapper_) { + Blockly.unbindEvent_(this.onMouseDownResizeWrapper_); + } Blockly.Bubble.unbindDragEvents_(); - // Dispose of and unlink the bubble. Blockly.utils.dom.removeNode(this.bubbleGroup_); - this.bubbleGroup_ = null; - this.bubbleArrow_ = null; - this.bubbleBack_ = null; - this.resizeGroup_ = null; - this.workspace_ = null; - this.content_ = null; - this.shape_ = null; + this.disposed = true; }; /** @@ -798,7 +849,9 @@ Blockly.Bubble.prototype.moveDuringDrag = function(dragSurface, newLoc) { */ Blockly.Bubble.prototype.getRelativeToSurfaceXY = function() { return new Blockly.utils.Coordinate( - this.anchorXY_.x + this.relativeLeft_, + this.workspace_.RTL ? + -this.relativeLeft_ + this.anchorXY_.x - this.width_ : + this.anchorXY_.x + this.relativeLeft_, this.anchorXY_.y + this.relativeTop_); }; diff --git a/core/bubble_dragger.js b/core/bubble_dragger.js index e19bb6169..9a26c73c6 100644 --- a/core/bubble_dragger.js +++ b/core/bubble_dragger.js @@ -92,6 +92,7 @@ Blockly.BubbleDragger = function(bubble, workspace) { /** * Sever all links from this object. * @package + * @suppress {checkTypes} */ Blockly.BubbleDragger.prototype.dispose = function() { this.draggingBubble_ = null; @@ -178,12 +179,12 @@ Blockly.BubbleDragger.prototype.updateCursorDuringBubbleDrag_ = function() { if (this.wouldDeleteBubble_) { this.draggingBubble_.setDeleteStyle(true); if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) { - trashcan.setOpen_(true); + trashcan.setOpen(true); } } else { this.draggingBubble_.setDeleteStyle(false); if (trashcan) { - trashcan.setOpen_(false); + trashcan.setOpen(false); } } }; @@ -218,10 +219,10 @@ Blockly.BubbleDragger.prototype.endBubbleDrag = function( } this.workspace_.setResizesEnabled(true); - if (this.workspace_.toolbox_) { + if (this.workspace_.getToolbox()) { var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' : 'blocklyToolboxGrab'; - this.workspace_.toolbox_.removeStyle(style); + this.workspace_.getToolbox().removeStyle(style); } Blockly.Events.setGroup(false); }; @@ -232,7 +233,8 @@ Blockly.BubbleDragger.prototype.endBubbleDrag = function( */ Blockly.BubbleDragger.prototype.fireMoveEvent_ = function() { if (this.draggingBubble_.isComment) { - var event = new Blockly.Events.CommentMove(this.draggingBubble_); + var event = new Blockly.Events.CommentMove( + /** @type {!Blockly.WorkspaceCommentSvg} */ (this.draggingBubble_)); event.setOldCoordinate(this.startXY_); event.recordNew(); Blockly.Events.fire(event); @@ -265,6 +267,7 @@ Blockly.BubbleDragger.prototype.pixelsToWorkspaceUnits_ = function(pixelCoord) { } return result; }; + /** * Move the bubble onto the drag surface at the beginning of a drag. Move the * drag surface to preserve the apparent location of the bubble. diff --git a/core/comment.js b/core/comment.js index ce2b7b642..b2ef69bf7 100644 --- a/core/comment.js +++ b/core/comment.js @@ -24,6 +24,7 @@ goog.provide('Blockly.Comment'); goog.require('Blockly.Bubble'); +goog.require('Blockly.Css'); goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockChange'); goog.require('Blockly.Events.Ui'); @@ -61,6 +62,34 @@ Blockly.Comment = function(block) { */ this.cachedText_ = ''; + /** + * Mouse up event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseUpWrapper_ = null; + + /** + * Wheel event data. + * @type {?Blockly.EventData} + * @private + */ + this.onWheelWrapper_ = null; + + /** + * Change event data. + * @type {?Blockly.EventData} + * @private + */ + this.onChangeWrapper_ = null; + + /** + * Input event data. + * @type {?Blockly.EventData} + * @private + */ + this.onInputWrapper_ = null; + this.createIcon(); }; Blockly.utils.object.inherits(Blockly.Comment, Blockly.Icon); @@ -137,21 +166,24 @@ Blockly.Comment.prototype.createEditor_ = function() { // Ideally this would be hooked to the focus event for the comment. // However doing so in Firefox swallows the cursor for unknown reasons. // So this is hooked to mouseup instead. No big deal. - Blockly.bindEventWithChecks_(textarea, 'mouseup', this, this.startEdit_, - true, true); + this.onMouseUpWrapper_ = Blockly.bindEventWithChecks_( + textarea, 'mouseup', this, this.startEdit_, true, true); // Don't zoom with mousewheel. - Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) { - e.stopPropagation(); - }); - Blockly.bindEventWithChecks_(textarea, 'change', this, function(_e) { - if (this.cachedText_ != this.model_.text) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.block_, 'comment', null, this.cachedText_, this.model_.text)); - } - }); - Blockly.bindEventWithChecks_(textarea, 'input', this, function(_e) { - this.model_.text = textarea.value; - }); + this.onWheelWrapper_ = Blockly.bindEventWithChecks_( + textarea, 'wheel', this, function(e) { + e.stopPropagation(); + }); + this.onChangeWrapper_ = Blockly.bindEventWithChecks_( + textarea, 'change', this, function(_e) { + if (this.cachedText_ != this.model_.text) { + Blockly.Events.fire(new Blockly.Events.BlockChange( + this.block_, 'comment', null, this.cachedText_, this.model_.text)); + } + }); + this.onInputWrapper_ = Blockly.bindEventWithChecks_( + textarea, 'input', this, function(_e) { + this.model_.text = textarea.value; + }); setTimeout(textarea.focus.bind(textarea), 0); @@ -241,17 +273,19 @@ Blockly.Comment.prototype.createBubble_ = function() { Blockly.Comment.prototype.createEditableBubble_ = function() { this.bubble_ = new Blockly.Bubble( /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), - this.createEditor_(), this.block_.svgPath_, - this.iconXY_, this.model_.size.width, this.model_.size.height); + this.createEditor_(), this.block_.pathObject.svgPath, + /** @type {!Blockly.utils.Coordinate} */ (this.iconXY_), + this.model_.size.width, this.model_.size.height); // Expose this comment's block's ID on its top-level SVG group. this.bubble_.setSvgId(this.block_.id); this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this)); - this.updateColour(); + this.applyColour(); }; /** * Show a non-editable bubble. * @private + * @suppress {checkTypes} Suppress `this` type mismatch. */ Blockly.Comment.prototype.createNonEditableBubble_ = function() { // TODO (#2917): It would be great if the comment could support line breaks. @@ -261,6 +295,7 @@ Blockly.Comment.prototype.createNonEditableBubble_ = function() { /** * Dispose of the bubble. * @private + * @suppress {checkTypes} Suppress `this` type mismatch. */ Blockly.Comment.prototype.disposeBubble_ = function() { if (this.paragraphElement_) { @@ -268,7 +303,22 @@ Blockly.Comment.prototype.disposeBubble_ = function() { Blockly.Warning.prototype.disposeBubble.call(this); return; } - + if (this.onMouseUpWrapper_) { + Blockly.unbindEvent_(this.onMouseUpWrapper_); + this.onMouseUpWrapper_ = null; + } + if (this.onWheelWrapper_) { + Blockly.unbindEvent_(this.onWheelWrapper_); + this.onWheelWrapper_ = null; + } + if (this.onChangeWrapper_) { + Blockly.unbindEvent_(this.onChangeWrapper_); + this.onChangeWrapper_ = null; + } + if (this.onInputWrapper_) { + Blockly.unbindEvent_(this.onInputWrapper_); + this.onInputWrapper_ = null; + } this.bubble_.dispose(); this.bubble_ = null; this.textarea_ = null; @@ -284,7 +334,7 @@ Blockly.Comment.prototype.disposeBubble_ = function() { * @private */ Blockly.Comment.prototype.startEdit_ = function(_e) { - if (this.bubble_.promote_()) { + if (this.bubble_.promote()) { // Since the act of moving this node within the DOM causes a loss of focus, // we need to reapply the focus. this.textarea_.focus(); @@ -364,3 +414,21 @@ Blockly.Comment.prototype.dispose = function() { this.block_.comment = null; Blockly.Icon.prototype.dispose.call(this); }; + +/** + * CSS for block comment. See css.js for use. + */ +Blockly.Css.register([ + /* eslint-disable indent */ + '.blocklyCommentTextarea {', + 'background-color: #fef49c;', + 'border: 0;', + 'outline: 0;', + 'margin: 0;', + 'padding: 3px;', + 'resize: none;', + 'display: block;', + 'overflow: hidden;', + '}' + /* eslint-enable indent */ +]); diff --git a/core/components/component.js b/core/components/component.js index 82507405c..86e55fab8 100644 --- a/core/components/component.js +++ b/core/components/component.js @@ -339,7 +339,7 @@ Blockly.Component.prototype.exitDocument = function() { /** * Disposes of the object. If the object hasn't already been disposed of, calls * {@link #disposeInternal}. - * @protected + * @package */ Blockly.Component.prototype.dispose = function() { if (!this.disposed_) { diff --git a/core/components/menu/menu.js b/core/components/menu/menu.js index 00fd13e4e..40f517469 100644 --- a/core/components/menu/menu.js +++ b/core/components/menu/menu.js @@ -25,6 +25,7 @@ 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'); @@ -37,6 +38,15 @@ goog.require('Blockly.utils.object'); 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. @@ -44,6 +54,41 @@ Blockly.Menu = function() { * @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); @@ -73,7 +118,7 @@ Blockly.Menu.prototype.createDom = function() { Blockly.Menu.prototype.focus = function() { var el = this.getElement(); if (el) { - el.focus(); + el.focus({preventScroll:true}); Blockly.utils.dom.addClass(el, 'focused'); } }; @@ -92,7 +137,7 @@ Blockly.Menu.prototype.blur = function() { /** * Set the menu accessibility role. - * @param {!Blockly.utils.aria.Role|string} roleName role name. + * @param {!Blockly.utils.aria.Role} roleName role name. * @package */ Blockly.Menu.prototype.setRole = function(roleName) { @@ -147,7 +192,6 @@ Blockly.Menu.prototype.attachEvents_ = function() { 'mouseenter', this, this.handleMouseEnter_, true); this.mouseLeaveHandler_ = Blockly.bindEventWithChecks_(el, 'mouseleave', this, this.handleMouseLeave_, true); - this.onKeyDownWrapper_ = Blockly.bindEventWithChecks_(el, 'keydown', this, this.handleKeyEvent); }; @@ -157,11 +201,26 @@ Blockly.Menu.prototype.attachEvents_ = function() { * @private */ Blockly.Menu.prototype.detachEvents_ = function() { - Blockly.unbindEvent_(this.mouseOverHandler_); - Blockly.unbindEvent_(this.clickHandler_); - Blockly.unbindEvent_(this.mouseEnterHandler_); - Blockly.unbindEvent_(this.mouseLeaveHandler_); - Blockly.unbindEvent_(this.onKeyDownWrapper_); + 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. @@ -383,6 +442,20 @@ Blockly.Menu.prototype.handleMouseOver_ = function(e) { * @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)) { diff --git a/core/components/menu/menuitem.js b/core/components/menu/menuitem.js index 7e529279d..9639aab43 100644 --- a/core/components/menu/menuitem.js +++ b/core/components/menu/menuitem.js @@ -95,8 +95,8 @@ Blockly.MenuItem.prototype.createDom = function() { 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); + Blockly.utils.aria.setState(element, Blockly.utils.aria.State.SELECTED, + (this.checkable_ && this.checked_) || false); }; /** @@ -164,7 +164,7 @@ Blockly.MenuItem.prototype.getValue = function() { /** * Set the menu accessibility role. - * @param {!Blockly.utils.aria.Role|string} roleName role name. + * @param {!Blockly.utils.aria.Role} roleName Role name. * @package */ Blockly.MenuItem.prototype.setRole = function(roleName) { diff --git a/core/components/tree/basenode.js b/core/components/tree/basenode.js index 9127dbe32..40130fb71 100644 --- a/core/components/tree/basenode.js +++ b/core/components/tree/basenode.js @@ -188,10 +188,8 @@ Blockly.tree.BaseNode.prototype.initAccessibility = function() { label.id = this.getId() + '.label'; } - Blockly.utils.aria.setRole(el, - Blockly.utils.aria.Role.TREEITEM); - Blockly.utils.aria.setState(el, - Blockly.utils.aria.State.SELECTED, false); + Blockly.utils.aria.setRole(el, Blockly.utils.aria.Role.TREEITEM); + Blockly.utils.aria.setState(el, Blockly.utils.aria.State.SELECTED, false); Blockly.utils.aria.setState(el, Blockly.utils.aria.State.LEVEL, this.getDepth()); if (label) { diff --git a/core/components/tree/treecontrol.js b/core/components/tree/treecontrol.js index 7e9a58659..f42c8089a 100644 --- a/core/components/tree/treecontrol.js +++ b/core/components/tree/treecontrol.js @@ -44,6 +44,34 @@ 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} + * @private + */ + this.onClickWrapper_ = null; + + /** + * Key down event data. + * @type {?Blockly.EventData} + * @private + */ + this.onKeydownWrapper_ = null; + Blockly.tree.BaseNode.call(this, '', config); // The root is open and selected by default. @@ -263,7 +291,6 @@ Blockly.tree.TreeControl.prototype.exitDocument = function() { /** * Adds the event listeners to the tree. * @private - * @suppress {deprecated} Suppress deprecated bindEvent_ call. */ Blockly.tree.TreeControl.prototype.attachEvents_ = function() { var el = this.getElement(); @@ -273,10 +300,8 @@ Blockly.tree.TreeControl.prototype.attachEvents_ = function() { '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, 'keydown', this, this.handleKeyEvent_); }; @@ -286,10 +311,22 @@ Blockly.tree.TreeControl.prototype.attachEvents_ = function() { * @private */ Blockly.tree.TreeControl.prototype.detachEvents_ = function() { - Blockly.unbindEvent_(this.onFocusWrapper_); - Blockly.unbindEvent_(this.onBlurWrapper_); - Blockly.unbindEvent_(this.onClickWrapper_); - Blockly.unbindEvent_(this.onKeydownWrapper_); + 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; + } + if (this.onKeydownWrapper_) { + Blockly.unbindEvent_(this.onKeydownWrapper_); + this.onKeydownWrapper_ = null; + } }; /** diff --git a/core/connection.js b/core/connection.js index 90c38685d..4bc0e40b7 100644 --- a/core/connection.js +++ b/core/connection.js @@ -85,16 +85,16 @@ Blockly.Connection.prototype.shadowDom_ = null; /** * Horizontal location of this connection. * @type {number} - * @protected + * @package */ -Blockly.Connection.prototype.x_ = 0; +Blockly.Connection.prototype.x = 0; /** * Vertical location of this connection. * @type {number} - * @protected + * @package */ -Blockly.Connection.prototype.y_ = 0; +Blockly.Connection.prototype.y = 0; /** * Connect two connections together. This is the connection on the superior @@ -121,7 +121,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { if (orphanBlock.isShadow()) { // Save the shadow block so that field values are preserved. shadowDom = Blockly.Xml.blockToDom(orphanBlock); - orphanBlock.dispose(); + orphanBlock.dispose(false); orphanBlock = null; } else if (parentConnection.type == Blockly.INPUT_VALUE) { // Value connections. @@ -132,7 +132,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { // Attempt to reattach the orphan at the end of the newly inserted // block. Since this block may be a row, walk down to the end // or to the first (and only) shadow block. - var connection = Blockly.Connection.lastConnectionInRow_( + var connection = Blockly.Connection.lastConnectionInRow( childBlock, orphanBlock); if (connection) { orphanBlock.outputConnection.connect(connection); @@ -153,7 +153,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { if (nextBlock && !nextBlock.isShadow()) { newBlock = nextBlock; } else { - if (orphanBlock.previousConnection.checkType_( + if (orphanBlock.previousConnection.checkType( newBlock.nextConnection)) { newBlock.nextConnection.connect(orphanBlock.previousConnection); orphanBlock = null; @@ -201,8 +201,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { }; /** - * Dispose of this connection. Deal with connected blocks and remove this - * connection from the database. + * Dispose of this connection and deal with connected blocks. * @package */ Blockly.Connection.prototype.dispose = function() { @@ -213,7 +212,7 @@ Blockly.Connection.prototype.dispose = function() { var targetBlock = this.targetBlock(); if (targetBlock.isShadow()) { // Destroy the attached shadow block & its children. - targetBlock.dispose(); + targetBlock.dispose(false); } else { // Disconnect the attached normal block. targetBlock.unplug(); @@ -225,7 +224,7 @@ Blockly.Connection.prototype.dispose = function() { /** * Get the source block for this connection. - * @return {Blockly.Block} The source block, or null if there is none. + * @return {!Blockly.Block} The source block. */ Blockly.Connection.prototype.getSourceBlock = function() { return this.sourceBlock_; @@ -254,9 +253,9 @@ 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. - * @private + * @package */ -Blockly.Connection.prototype.canConnectWithReason_ = function(target) { +Blockly.Connection.prototype.canConnectWithReason = function(target) { if (!target) { return Blockly.Connection.REASON_TARGET_NULL; } @@ -273,7 +272,7 @@ Blockly.Connection.prototype.canConnectWithReason_ = function(target) { return Blockly.Connection.REASON_WRONG_TYPE; } else if (blockA && blockB && blockA.workspace !== blockB.workspace) { return Blockly.Connection.REASON_DIFFERENT_WORKSPACES; - } else if (!this.checkType_(target)) { + } else if (!this.checkType(target)) { return Blockly.Connection.REASON_CHECKS_FAILED; } else if (blockA.isShadow() && !blockB.isShadow()) { return Blockly.Connection.REASON_SHADOW_PARENT; @@ -286,10 +285,10 @@ Blockly.Connection.prototype.canConnectWithReason_ = function(target) { * and throws an exception if they are not. * @param {Blockly.Connection} target The connection to check compatibility * with. - * @private + * @package */ -Blockly.Connection.prototype.checkConnection_ = function(target) { - switch (this.canConnectWithReason_(target)) { +Blockly.Connection.prototype.checkConnection = function(target) { + switch (this.canConnectWithReason(target)) { case Blockly.Connection.CAN_CONNECT: break; case Blockly.Connection.REASON_SELF_CONNECTION: @@ -329,7 +328,7 @@ Blockly.Connection.prototype.canConnectToPrevious_ = function(candidate) { } // Don't let blocks try to connect to themselves or ones they nest. - if (Blockly.draggingConnections_.indexOf(candidate) != -1) { + if (Blockly.draggingConnections.indexOf(candidate) != -1) { return false; } @@ -359,7 +358,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { return false; } // Type checking. - var canConnect = this.canConnectWithReason_(candidate); + var canConnect = this.canConnectWithReason(candidate); if (canConnect != Blockly.Connection.CAN_CONNECT) { return false; } @@ -406,7 +405,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { } // Don't let blocks try to connect to themselves or ones they nest. - if (Blockly.draggingConnections_.indexOf(candidate) != -1) { + if (Blockly.draggingConnections.indexOf(candidate) != -1) { return false; } @@ -415,7 +414,7 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { /** * Behavior after a connection attempt fails. - * @param {Blockly.Connection} _otherConnection Connection that this connection + * @param {!Blockly.Connection} _otherConnection Connection that this connection * failed to connect to. * @package */ @@ -432,7 +431,7 @@ Blockly.Connection.prototype.connect = function(otherConnection) { // Already connected together. NOP. return; } - this.checkConnection_(otherConnection); + this.checkConnection(otherConnection); var eventGroup = Blockly.Events.getGroup(); if (!eventGroup) { Blockly.Events.setGroup(true); @@ -474,11 +473,11 @@ Blockly.Connection.connectReciprocally_ = function(first, second) { * @private */ Blockly.Connection.singleConnection_ = function(block, orphanBlock) { - var connection = false; + var connection = null; for (var i = 0; i < block.inputList.length; i++) { var thisConnection = block.inputList[i].connection; if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE && - orphanBlock.outputConnection.checkType_(thisConnection)) { + orphanBlock.outputConnection.checkType(thisConnection)) { if (connection) { return null; // More than one connection. } @@ -497,15 +496,14 @@ Blockly.Connection.singleConnection_ = function(block, orphanBlock) { * @param {!Blockly.Block} startBlock The block on which to start the search. * @param {!Blockly.Block} orphanBlock The block that is looking for a home. * @return {Blockly.Connection} The suitable connection point on the chain - * of blocks, or null. - * @private + * of blocks, or null. + * @package */ -Blockly.Connection.lastConnectionInRow_ = function(startBlock, orphanBlock) { +Blockly.Connection.lastConnectionInRow = function(startBlock, orphanBlock) { var newBlock = startBlock; var connection; - while (connection = Blockly.Connection.singleConnection_( - /** @type {!Blockly.Block} */ (newBlock), orphanBlock)) { - // '=' is intentional in line above. + while ((connection = Blockly.Connection.singleConnection_( + /** @type {!Blockly.Block} */ (newBlock), orphanBlock))) { newBlock = connection.targetBlock(); if (!newBlock || newBlock.isShadow()) { return connection; @@ -607,9 +605,8 @@ Blockly.Connection.prototype.targetBlock = function() { * value type system. E.g. square_root("Hello") is not compatible. * @param {!Blockly.Connection} otherConnection Connection to compare against. * @return {boolean} True if the connections share a type. - * @protected */ -Blockly.Connection.prototype.checkType_ = function(otherConnection) { +Blockly.Connection.prototype.checkType = function(otherConnection) { if (!this.check_ || !otherConnection.check_) { // One or both sides are promiscuous enough that anything will fit. return true; @@ -625,12 +622,27 @@ Blockly.Connection.prototype.checkType_ = function(otherConnection) { }; /** - * Function to be called when this connection's compatible types have changed. + * Is this connection compatible with another connection with respect to the + * value type system. E.g. square_root("Hello") is not compatible. + * @param {!Blockly.Connection} otherConnection Connection to compare against. + * @return {boolean} True if the connections share a type. * @private + * @deprecated October 2019, use connection.checkType instead. + */ +Blockly.Connection.prototype.checkType_ = function(otherConnection) { + console.warn('Deprecated call to Blockly.Connection.prototype.checkType_, ' + + 'use Blockly.Connection.prototype.checkType instead.'); + return this.checkType(otherConnection); +}; + +/** + * Function to be called when this connection's compatible types have changed. + * @protected */ Blockly.Connection.prototype.onCheckChanged_ = function() { // The new value type may not be compatible with the existing connection. - if (this.isConnected() && !this.checkType_(this.targetConnection)) { + if (this.isConnected() && (!this.targetConnection || + !this.checkType(this.targetConnection))) { var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; child.unplug(); } @@ -638,8 +650,8 @@ Blockly.Connection.prototype.onCheckChanged_ = function() { /** * Change a connection's compatibility. - * @param {string|!Array} check Compatible value type or list of value - * types. Null if all types are compatible. + * @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). */ @@ -693,9 +705,9 @@ Blockly.Connection.prototype.getShadowDom = function() { * computed from the rendered positioning. * @param {number} _maxLimit The maximum radius to another connection. * @return {!Array.} List of connections. - * @private + * @package */ -Blockly.Connection.prototype.neighbours_ = function(_maxLimit) { +Blockly.Connection.prototype.neighbours = function(_maxLimit) { return []; }; @@ -736,7 +748,7 @@ Blockly.Connection.prototype.toString = function() { msg = 'Next Connection of '; } else { var parentInput = null; - for (var i = 0, input; input = block.inputList[i]; i++) { + for (var i = 0, input; (input = block.inputList[i]); i++) { if (input.connection == this) { parentInput = input; break; diff --git a/core/connection_db.js b/core/connection_db.js index d4a2fc213..8ff7c7683 100644 --- a/core/connection_db.js +++ b/core/connection_db.js @@ -16,14 +16,16 @@ */ /** - * @fileoverview Components for managing connections between blocks. + * @fileoverview A database of all the rendered connections that could + * possibly be connected to (i.e. not collapsed, etc). + * Sorted by y coordinate. * @author fraser@google.com (Neil Fraser) */ 'use strict'; goog.provide('Blockly.ConnectionDB'); -goog.require('Blockly.Connection'); +goog.require('Blockly.RenderedConnection'); /** @@ -34,54 +36,52 @@ goog.require('Blockly.Connection'); */ Blockly.ConnectionDB = function() { /** - * Array of connections sorted by y coordinate. - * @type {!Array.} + * Array of connections sorted by y position in workspace units. + * @type {!Array.} * @private */ this.connections_ = []; }; /** - * Add a connection to the database. Must not already exist in DB. - * @param {!Blockly.Connection} connection The connection to be added. + * Add a connection to the database. Should not already exist in the database. + * @param {!Blockly.RenderedConnection} connection The connection to be added. + * @param {number} yPos The y position used to decide where to insert the + * connection. + * @package */ -Blockly.ConnectionDB.prototype.addConnection = function(connection) { - if (connection.inDB_) { - throw Error('Connection already in database.'); - } - if (connection.getSourceBlock().isInFlyout) { - // Don't bother maintaining a database of connections in a flyout. - return; - } - var position = this.findPositionForConnection_(connection); - this.connections_.splice(position, 0, connection); - connection.inDB_ = true; +Blockly.ConnectionDB.prototype.addConnection = function(connection, yPos) { + var index = this.calculateIndexForYPos_(yPos); + this.connections_.splice(index, 0, connection); }; /** - * Find the given connection. + * Finds the index of the given connection. + * * Starts by doing a binary search to find the approximate location, then - * linearly searches nearby for the exact connection. - * @param {!Blockly.Connection} conn The connection to find. + * linearly searches nearby for the exact connection. + * @param {!Blockly.RenderedConnection} conn The connection to find. + * @param {number} yPos The y position used to find the index of the connection. * @return {number} The index of the connection, or -1 if the connection was * not found. + * @private */ -Blockly.ConnectionDB.prototype.findConnection = function(conn) { +Blockly.ConnectionDB.prototype.findIndexOfConnection_ = function(conn, yPos) { if (!this.connections_.length) { return -1; } - var bestGuess = this.findPositionForConnection_(conn); + var bestGuess = this.calculateIndexForYPos_(yPos); if (bestGuess >= this.connections_.length) { // Not in list return -1; } - var yPos = conn.y_; + yPos = conn.y; // Walk forward and back on the y axis looking for the connection. var pointerMin = bestGuess; var pointerMax = bestGuess; - while (pointerMin >= 0 && this.connections_[pointerMin].y_ == yPos) { + while (pointerMin >= 0 && this.connections_[pointerMin].y == yPos) { if (this.connections_[pointerMin] == conn) { return pointerMin; } @@ -89,7 +89,7 @@ Blockly.ConnectionDB.prototype.findConnection = function(conn) { } while (pointerMax < this.connections_.length && - this.connections_[pointerMax].y_ == yPos) { + this.connections_[pointerMax].y == yPos) { if (this.connections_[pointerMax] == conn) { return pointerMax; } @@ -99,15 +99,13 @@ Blockly.ConnectionDB.prototype.findConnection = function(conn) { }; /** - * Finds a candidate position for inserting this connection into the list. - * This will be in the correct y order but makes no guarantees about ordering in - * the x axis. - * @param {!Blockly.Connection} connection The connection to insert. + * Finds the correct index for the given y position. + * @param {number} yPos The y position used to decide where to + * insert the connection. * @return {number} The candidate index. * @private */ -Blockly.ConnectionDB.prototype.findPositionForConnection_ = function( - connection) { +Blockly.ConnectionDB.prototype.calculateIndexForYPos_ = function(yPos) { if (!this.connections_.length) { return 0; } @@ -115,9 +113,9 @@ Blockly.ConnectionDB.prototype.findPositionForConnection_ = function( var pointerMax = this.connections_.length; while (pointerMin < pointerMax) { var pointerMid = Math.floor((pointerMin + pointerMax) / 2); - if (this.connections_[pointerMid].y_ < connection.y_) { + if (this.connections_[pointerMid].y < yPos) { pointerMin = pointerMid + 1; - } else if (this.connections_[pointerMid].y_ > connection.y_) { + } else if (this.connections_[pointerMid].y > yPos) { pointerMax = pointerMid; } else { pointerMin = pointerMid; @@ -129,40 +127,37 @@ Blockly.ConnectionDB.prototype.findPositionForConnection_ = function( /** * Remove a connection from the database. Must already exist in DB. - * @param {!Blockly.Connection} connection The connection to be removed. - * @private + * @param {!Blockly.RenderedConnection} connection The connection to be removed. + * @param {number} yPos The y position used to find the index of the connection. + * @throws {Error} If the connection cannot be found in the database. */ -Blockly.ConnectionDB.prototype.removeConnection_ = function(connection) { - if (!connection.inDB_) { - throw Error('Connection not in database.'); - } - var removalIndex = this.findConnection(connection); - if (removalIndex == -1) { +Blockly.ConnectionDB.prototype.removeConnection = function(connection, yPos) { + var index = this.findIndexOfConnection_(connection, yPos); + if (index == -1) { throw Error('Unable to find connection in connectionDB.'); } - connection.inDB_ = false; - this.connections_.splice(removalIndex, 1); + this.connections_.splice(index, 1); }; /** * Find all nearby connections to the given connection. * Type checking does not apply, since this function is used for bumping. - * @param {!Blockly.Connection} connection The connection whose neighbours - * should be returned. + * @param {!Blockly.RenderedConnection} connection The connection whose + * neighbours should be returned. * @param {number} maxRadius The maximum radius to another connection. - * @return {!Array.} List of connections. + * @return {!Array.} List of connections. */ Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { var db = this.connections_; - var currentX = connection.x_; - var currentY = connection.y_; + var currentX = connection.x; + var currentY = connection.y; // Binary search to find the closest y location. var pointerMin = 0; var pointerMax = db.length - 2; var pointerMid = pointerMax; while (pointerMin < pointerMid) { - if (db[pointerMid].y_ < currentY) { + if (db[pointerMid].y < currentY) { pointerMin = pointerMid; } else { pointerMax = pointerMid; @@ -180,8 +175,8 @@ Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { * the other connection is less than the allowed radius. */ function checkConnection_(yIndex) { - var dx = currentX - db[yIndex].x_; - var dy = currentY - db[yIndex].y_; + var dx = currentX - db[yIndex].x; + var dy = currentY - db[yIndex].y; var r = Math.sqrt(dx * dx + dy * dy); if (r <= maxRadius) { neighbours.push(db[yIndex]); @@ -204,7 +199,6 @@ Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { return neighbours; }; - /** * Is the candidate connection close to the reference connection. * Extremely fast; only looks at Y distance. @@ -215,20 +209,20 @@ Blockly.ConnectionDB.prototype.getNeighbours = function(connection, maxRadius) { * @private */ Blockly.ConnectionDB.prototype.isInYRange_ = function(index, baseY, maxRadius) { - return (Math.abs(this.connections_[index].y_ - baseY) <= maxRadius); + return (Math.abs(this.connections_[index].y - baseY) <= maxRadius); }; /** * Find the closest compatible connection to this connection. - * @param {!Blockly.Connection} conn The connection searching for a compatible + * @param {!Blockly.RenderedConnection} conn The connection searching for a compatible * mate. * @param {number} maxRadius The maximum radius to another connection. * @param {!Blockly.utils.Coordinate} dxy Offset between this connection's * location in the database and the current location (as a result of * dragging). - * @return {!{connection: ?Blockly.Connection, radius: number}} Contains two - * properties:' connection' which is either another connection or null, - * and 'radius' which is the distance. + * @return {!{connection: Blockly.RenderedConnection, radius: number}} + * Contains two properties: 'connection' which is either another + * connection or null, and 'radius' which is the distance. */ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, dxy) { @@ -238,16 +232,16 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, } // Stash the values of x and y from before the drag. - var baseY = conn.y_; - var baseX = conn.x_; + var baseY = conn.y; + var baseX = conn.x; - conn.x_ = baseX + dxy.x; - conn.y_ = baseY + dxy.y; + conn.x = baseX + dxy.x; + conn.y = baseY + dxy.y; - // findPositionForConnection finds an index for insertion, which is always + // calculateIndexForYPos_ finds an index for insertion, which is always // after any block with the same y index. We want to search both forward // and back, so search on both sides of the index. - var closestIndex = this.findPositionForConnection_(conn); + var closestIndex = this.calculateIndexForYPos_(conn.y); var bestConnection = null; var bestRadius = maxRadius; @@ -255,7 +249,7 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, // Walk forward and back on the y axis looking for the closest x,y point. var pointerMin = closestIndex - 1; - while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y_, maxRadius)) { + while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) { temp = this.connections_[pointerMin]; if (conn.isConnectionAllowed(temp, bestRadius)) { bestConnection = temp; @@ -266,7 +260,7 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, var pointerMax = closestIndex; while (pointerMax < this.connections_.length && - this.isInYRange_(pointerMax, conn.y_, maxRadius)) { + this.isInYRange_(pointerMax, conn.y, maxRadius)) { temp = this.connections_[pointerMax]; if (conn.isConnectionAllowed(temp, bestRadius)) { bestConnection = temp; @@ -276,8 +270,8 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, } // Reset the values of x and y. - conn.x_ = baseX; - conn.y_ = baseY; + conn.x = baseX; + conn.y = baseY; // If there were no valid connections, bestConnection will be null. return {connection: bestConnection, radius: bestRadius}; diff --git a/core/contextmenu.js b/core/contextmenu.js index 5d19bd0f7..1c51704f7 100644 --- a/core/contextmenu.js +++ b/core/contextmenu.js @@ -89,7 +89,7 @@ Blockly.ContextMenu.populate_ = function(options, rtl) { */ var menu = new Blockly.Menu(); menu.setRightToLeft(rtl); - for (var i = 0, option; option = options[i]; i++) { + for (var i = 0, option; (option = options[i]); i++) { var menuItem = new Blockly.MenuItem(option.text); menuItem.setRightToLeft(rtl); menu.addChild(menuItem, true); @@ -166,6 +166,7 @@ Blockly.ContextMenu.hide = function() { Blockly.ContextMenu.currentBlock = null; if (Blockly.ContextMenu.eventWrapper_) { Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_); + Blockly.ContextMenu.eventWrapper_ = null; } }; @@ -243,7 +244,7 @@ Blockly.ContextMenu.blockHelpOption = function(block) { enabled: !!url, text: Blockly.Msg['HELP'], callback: function() { - block.showHelp_(); + block.showHelp(); } }; return helpOption; @@ -261,7 +262,7 @@ Blockly.ContextMenu.blockDuplicateOption = function(block) { text: Blockly.Msg['DUPLICATE_BLOCK'], enabled: enabled, callback: function() { - Blockly.duplicate_(block); + Blockly.duplicate(block); } }; return duplicateOption; @@ -279,7 +280,7 @@ Blockly.ContextMenu.blockCommentOption = function(block) { enabled: !Blockly.utils.userAgent.IE }; // If there's already a comment, add an option to delete it. - if (block.comment) { + if (block.getCommentIcon()) { commentOption.text = Blockly.Msg['REMOVE_COMMENT']; commentOption.callback = function() { block.setCommentText(null); @@ -303,7 +304,7 @@ Blockly.ContextMenu.blockCommentOption = function(block) { */ Blockly.ContextMenu.commentDeleteOption = function(comment) { var deleteOption = { - text: Blockly.Msg.REMOVE_COMMENT, + text: Blockly.Msg['REMOVE_COMMENT'], enabled: true, callback: function() { Blockly.Events.setGroup(true); @@ -323,10 +324,10 @@ Blockly.ContextMenu.commentDeleteOption = function(comment) { */ Blockly.ContextMenu.commentDuplicateOption = function(comment) { var duplicateOption = { - text: Blockly.Msg.DUPLICATE_COMMENT, + text: Blockly.Msg['DUPLICATE_COMMENT'], enabled: true, callback: function() { - Blockly.duplicate_(comment); + Blockly.duplicate(comment); } }; return duplicateOption; @@ -339,6 +340,8 @@ Blockly.ContextMenu.commentDuplicateOption = function(comment) { * @param {!Event} e The right-click mouse event. * @return {!Object} A menu option, containing text, enabled, and a callback. * @package + * @suppress {strictModuleDepCheck,checkTypes} Suppress checks while workspace + * comments are not bundled in. */ Blockly.ContextMenu.workspaceCommentOption = function(ws, e) { if (!Blockly.WorkspaceCommentSvg) { @@ -348,7 +351,7 @@ Blockly.ContextMenu.workspaceCommentOption = function(ws, e) { // location of the mouse event. var addWsComment = function() { var comment = new Blockly.WorkspaceCommentSvg( - ws, Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT, + ws, Blockly.Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'], Blockly.WorkspaceCommentSvg.DEFAULT_SIZE, Blockly.WorkspaceCommentSvg.DEFAULT_SIZE); @@ -388,7 +391,7 @@ Blockly.ContextMenu.workspaceCommentOption = function(ws, e) { // that they won't be able to edit. enabled: !Blockly.utils.userAgent.IE }; - wsCommentOption.text = Blockly.Msg.ADD_COMMENT; + wsCommentOption.text = Blockly.Msg['ADD_COMMENT']; wsCommentOption.callback = function() { addWsComment(); }; diff --git a/core/css.js b/core/css.js index ef9f17295..d62eaea51 100644 --- a/core/css.js +++ b/core/css.js @@ -77,6 +77,7 @@ Blockly.Css.inject = function(hasCss, pathToMedia) { // Inject CSS tag at start of head. var cssNode = document.createElement('style'); + cssNode.id = 'blockly-common-style'; var cssTextNode = document.createTextNode(text); cssNode.appendChild(cssTextNode); document.head.insertBefore(cssNode, document.head.firstChild); @@ -169,12 +170,14 @@ Blockly.Css.CONTENT = [ '}', '.blocklyDropDownDiv {', - 'position: fixed;', + '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);', @@ -213,14 +216,14 @@ Blockly.Css.CONTENT = [ 'cursor: pointer;', '}', - '.arrowTop {', + '.blocklyArrowTop {', 'border-top: 1px solid;', 'border-left: 1px solid;', 'border-top-left-radius: 4px;', 'border-color: inherit;', '}', - '.arrowBottom {', + '.blocklyArrowBottom {', 'border-bottom: 1px solid;', 'border-right: 1px solid;', 'border-bottom-right-radius: 4px;', @@ -254,11 +257,6 @@ Blockly.Css.CONTENT = [ 'stroke-width: 1;', '}', - '.blocklySelected>.blocklyPath {', - 'stroke: #fc3;', - 'stroke-width: 3px;', - '}', - '.blocklySelected>.blocklyPathLight {', 'display: none;', '}', @@ -325,22 +323,6 @@ Blockly.Css.CONTENT = [ 'stroke: none', '}', - '.blocklyReplaceable .blocklyPath {', - 'fill-opacity: .5;', - '}', - - '.blocklyReplaceable .blocklyPathLight,', - '.blocklyReplaceable .blocklyPathDark {', - 'display: none;', - '}', - - '.blocklyText {', - 'cursor: default;', - 'fill: #fff;', - 'font-family: sans-serif;', - 'font-size: 11pt;', - '}', - '.blocklyMultilineText {', 'font-family: monospace;', '}', @@ -349,22 +331,6 @@ Blockly.Css.CONTENT = [ 'pointer-events: none;', '}', - '.blocklyNonEditableText>rect,', - '.blocklyEditableText>rect {', - 'fill: #fff;', - 'fill-opacity: .6;', - '}', - - '.blocklyNonEditableText>text,', - '.blocklyEditableText>text {', - 'fill: #000;', - '}', - - '.blocklyEditableText:hover>rect {', - 'stroke: #fff;', - 'stroke-width: 2;', - '}', - '.blocklyBubbleText {', 'fill: #000;', '}', @@ -374,6 +340,10 @@ Blockly.Css.CONTENT = [ 'z-index: 20;', '}', + '.blocklyText text {', + 'cursor: default;', + '}', + /* Don't allow users to select text. It gets annoying when trying to drag a block and selected text moves instead. @@ -417,86 +387,17 @@ Blockly.Css.CONTENT = [ 'padding: 0;', '}', - '.blocklyCommentForeignObject {', - 'position: relative;', - 'z-index: 0;', - '}', - - '.blocklyCommentRect {', - 'fill: #E7DE8E;', - 'stroke: #bcA903;', - 'stroke-width: 1px', - '}', - - '.blocklyCommentTarget {', - 'fill: transparent;', - 'stroke: #bcA903;', - '}', - - '.blocklyCommentTargetFocused {', - 'fill: none;', - '}', - - '.blocklyCommentHandleTarget {', - 'fill: none;', - '}', - - '.blocklyCommentHandleTargetFocused {', - 'fill: transparent;', - '}', - - '.blocklyFocused>.blocklyCommentRect {', - 'fill: #B9B272;', - 'stroke: #B9B272;', - '}', - - '.blocklySelected>.blocklyCommentTarget {', - 'stroke: #fc3;', - 'stroke-width: 3px;', - '}', - - - '.blocklyCommentTextarea {', - 'background-color: #fef49c;', - 'border: 0;', - 'outline: 0;', - 'margin: 0;', - 'padding: 3px;', - 'resize: none;', - 'display: block;', - 'overflow: hidden;', - '}', - - '.blocklyCommentDeleteIcon {', - 'cursor: pointer;', - 'fill: #000;', - 'display: none', - '}', - - '.blocklySelected > .blocklyCommentDeleteIcon {', - 'display: block', - '}', - - '.blocklyDeleteIconShape {', - 'fill: #000;', - 'stroke: #000;', - 'stroke-width: 1px;', - '}', - - '.blocklyDeleteIconShape.blocklyDeleteIconHighlighted {', - 'stroke: #fc3;', - '}', - '.blocklyHtmlInput {', 'border: none;', 'border-radius: 4px;', - 'font-family: sans-serif;', 'height: 100%;', 'margin: 0;', 'outline: none;', 'padding: 0;', 'width: 100%;', 'text-align: center;', + 'display: block;', + 'box-sizing: border-box;', '}', /* Edge and IE introduce a close icon when the input value is longer than a @@ -588,7 +489,7 @@ Blockly.Css.CONTENT = [ 'padding-right: 28px;', '}', - '.blocklyVerticalCursor {', + '.blocklyVerticalMarker {', 'stroke-width: 3px;', 'fill: rgba(255,255,255,.5);', '}', @@ -698,7 +599,8 @@ Blockly.Css.CONTENT = [ '.blocklyWidgetDiv .goog-menuitem-content,', '.blocklyDropDownDiv .goog-menuitem-content {', - 'font: normal 13px Arial, sans-serif;', + 'font-family: Arial, sans-serif;', + 'font-size: 13px;', '}', '.blocklyWidgetDiv .goog-menuitem-content {', @@ -772,6 +674,16 @@ Blockly.Css.CONTENT = [ '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {', 'float: right;', 'margin-right: -24px;', - '}' + '}', + + '.blocklyComputeCanvas {', + 'position: absolute;', + 'width: 0;', + 'height: 0;', + '}', + + '.blocklyNoPointerEvents {', + 'pointer-events: none;', + '}', /* eslint-enable indent */ ]; diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index cb403bcbd..5f677f789 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -34,17 +34,11 @@ goog.require('Blockly.utils.style'); /** * Class for drop-down div. * @constructor + * @package */ Blockly.DropDownDiv = function() { }; -/** - * The div element. Set once by Blockly.DropDownDiv.createDom. - * @type {Element} - * @private - */ -Blockly.DropDownDiv.DIV_ = null; - /** * Drop-downs will appear within the bounds of this element if possible. * Set in Blockly.DropDownDiv.setBoundsElement. @@ -68,21 +62,24 @@ Blockly.DropDownDiv.owner_ = null; Blockly.DropDownDiv.positionToField_ = null; /** - * Arrow size in px. Should match the value in CSS (need to position pre-render). + * Arrow size in px. Should match the value in CSS + * (need to position pre-render). * @type {number} * @const */ Blockly.DropDownDiv.ARROW_SIZE = 16; /** - * Drop-down border size in px. Should match the value in CSS (need to position the arrow). + * Drop-down border size in px. Should match the value in CSS (need to position + * the arrow). * @type {number} * @const */ Blockly.DropDownDiv.BORDER_SIZE = 1; /** - * Amount the arrow must be kept away from the edges of the main drop-down div, in px. + * Amount the arrow must be kept away from the edges of the main drop-down div, + * in px. * @type {number} * @const */ @@ -102,35 +99,38 @@ Blockly.DropDownDiv.PADDING_Y = 16; */ Blockly.DropDownDiv.ANIMATION_TIME = 0.25; -/** - * The default dropdown div border color. - * @type {string} - * @const - */ -Blockly.DropDownDiv.DEFAULT_DROPDOWN_BORDER_COLOR = '#dadce0'; - -/** - * The default dropdown div color. - * @type {string} - * @const - */ -Blockly.DropDownDiv.DEFAULT_DROPDOWN_COLOR = '#fff'; - /** * Timer for animation out, to be cleared if we need to immediately hide * without disrupting new shows. * @type {?number} + * @private */ Blockly.DropDownDiv.animateOutTimer_ = null; /** * Callback for when the drop-down is hidden. * @type {?Function} + * @private */ Blockly.DropDownDiv.onHide_ = null; +/** + * A class name representing the current owner's workspace renderer. + * @type {?string} + * @private + */ +Blockly.DropDownDiv.rendererClassName_ = null; + +/** + * A class name representing the current owner's workspace theme. + * @type {?string} + * @private + */ +Blockly.DropDownDiv.themeClassName_ = null; + /** * Create and insert the DOM element for this div. + * @package */ Blockly.DropDownDiv.createDom = function() { if (Blockly.DropDownDiv.DIV_) { @@ -138,19 +138,32 @@ Blockly.DropDownDiv.createDom = function() { } var div = document.createElement('div'); div.className = 'blocklyDropDownDiv'; - div.style.backgroundColor = Blockly.DropDownDiv.DEFAULT_DROPDOWN_COLOR; - div.style.borderColor = Blockly.DropDownDiv.DEFAULT_DROPDOWN_BORDER_COLOR; document.body.appendChild(div); + /** + * The div element. + * @type {!Element} + * @private + */ Blockly.DropDownDiv.DIV_ = div; var content = document.createElement('div'); content.className = 'blocklyDropDownContent'; div.appendChild(content); + /** + * The content element. + * @type {!Element} + * @private + */ Blockly.DropDownDiv.content_ = content; var arrow = document.createElement('div'); arrow.className = 'blocklyDropDownArrow'; div.appendChild(arrow); + /** + * The arrow element. + * @type {!Element} + * @private + */ Blockly.DropDownDiv.arrow_ = arrow; Blockly.DropDownDiv.DIV_.style.opacity = 0; @@ -205,14 +218,6 @@ Blockly.DropDownDiv.setColour = function(backgroundColour, borderColour) { Blockly.DropDownDiv.DIV_.style.borderColor = borderColour; }; -/** - * Set the category for the drop-down. - * @param {string} category The new category for the drop-down. - */ -Blockly.DropDownDiv.setCategory = function(category) { - Blockly.DropDownDiv.DIV_.setAttribute('data-category', category); -}; - /** * Shortcut to show and place the drop-down with positioning determined * by a particular block. The primary position will be below the block, @@ -228,25 +233,9 @@ Blockly.DropDownDiv.setCategory = function(category) { */ Blockly.DropDownDiv.showPositionedByBlock = function(field, block, opt_onHide, opt_secondaryYOffset) { - var scale = block.workspace.scale; - var bBox = {width: block.width, height: block.height}; - bBox.width *= scale; - bBox.height *= scale; - var position = block.getSvgRoot().getBoundingClientRect(); - // If we can fit it, render below the block. - var primaryX = position.left + bBox.width / 2; - var primaryY = position.top + bBox.height; - // If we can't fit it, render above the entire parent block. - var secondaryX = primaryX; - var secondaryY = position.top; - if (opt_secondaryYOffset) { - secondaryY += opt_secondaryYOffset; - } - // Set bounds to workspace; show the drop-down. - Blockly.DropDownDiv.setBoundsElement( - block.workspace.getParentSvg().parentNode); - return Blockly.DropDownDiv.show( - field, block.RTL, primaryX, primaryY, secondaryX, secondaryY, opt_onHide); + return Blockly.DropDownDiv.showPositionedByRect_( + Blockly.DropDownDiv.getScaledBboxOfBlock_(block), + field, opt_onHide, opt_secondaryYOffset); }; /** @@ -263,19 +252,68 @@ Blockly.DropDownDiv.showPositionedByBlock = function(field, block, */ Blockly.DropDownDiv.showPositionedByField = function(field, opt_onHide, opt_secondaryYOffset) { - var position = field.getSvgRoot().getBoundingClientRect(); + Blockly.DropDownDiv.positionToField_ = true; + return Blockly.DropDownDiv.showPositionedByRect_( + Blockly.DropDownDiv.getScaledBboxOfField_(field), + field, opt_onHide, opt_secondaryYOffset); +}; + +/** + * Get the scaled bounding box of a block. + * @param {!Blockly.Block} block The block. + * @return {!Blockly.utils.Rect} The scaled bounding box of the block. + * @private + */ +Blockly.DropDownDiv.getScaledBboxOfBlock_ = function(block) { + var blockSvg = block.getSvgRoot(); + var bBox = blockSvg.getBBox(); + var scale = block.workspace.scale; + var scaledHeight = bBox.height * scale; + var scaledWidth = bBox.width * scale; + var xy = Blockly.utils.style.getPageOffset(blockSvg); + return new Blockly.utils.Rect( + xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth); +}; + +/** + * Get the scaled bounding box of a field. + * @param {!Blockly.Field} field The field. + * @return {!Blockly.utils.Rect} The scaled bounding box of the field. + * @private + */ +Blockly.DropDownDiv.getScaledBboxOfField_ = function(field) { + var bBox = field.getScaledBBox(); + return new Blockly.utils.Rect( + bBox.top, bBox.bottom, bBox.left, bBox.right); +}; + +/** + * Helper method to show and place the drop-down with positioning determined + * by a scaled bounding box. The primary position will be below the rect, + * and the secondary position above the rect. Drop-down will be constrained to + * the block's workspace. + * @param {!Blockly.utils.Rect} bBox The scaled bounding box. + * @param {!Blockly.Field} field The field to position the dropdown against. + * @param {Function=} opt_onHide Optional callback for when the drop-down is + * hidden. + * @param {number=} opt_secondaryYOffset Optional Y offset for above-block + * positioning. + * @return {boolean} True if the menu rendered below block; false if above. + * @private + */ +Blockly.DropDownDiv.showPositionedByRect_ = function(bBox, field, + opt_onHide, opt_secondaryYOffset) { // If we can fit it, render below the block. - var primaryX = position.left + position.width / 2; - var primaryY = position.bottom; + var primaryX = bBox.left + (bBox.right - bBox.left) / 2; + var primaryY = bBox.bottom; // If we can't fit it, render above the entire parent block. var secondaryX = primaryX; - var secondaryY = position.top; + var secondaryY = bBox.top; if (opt_secondaryYOffset) { secondaryY += opt_secondaryYOffset; } var sourceBlock = field.getSourceBlock(); // Set bounds to workspace; show the drop-down. - Blockly.DropDownDiv.positionToField_ = true; Blockly.DropDownDiv.setBoundsElement( sourceBlock.workspace.getParentSvg().parentNode); return Blockly.DropDownDiv.show( @@ -286,18 +324,21 @@ Blockly.DropDownDiv.showPositionedByField = function(field, /** * Show and place the drop-down. * The drop-down is placed with an absolute "origin point" (x, y) - i.e., - * the arrow will point at this origin and box will positioned below or above it. - * If we can maintain the container bounds at the primary point, the arrow will - * point there, and the container will be positioned below it. - * If we can't maintain the container bounds at the primary point, fall-back to the - * secondary point and position above. + * the arrow will point at this origin and box will positioned below or above + * it. If we can maintain the container bounds at the primary point, the arrow + * will point there, and the container will be positioned below it. + * If we can't maintain the container bounds at the primary point, fall-back to + * the secondary point and position above. * @param {Object} owner The object showing the drop-down * @param {boolean} rtl Right-to-left (true) or left-to-right (false). - * @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 - * @param {number} secondaryY Secondary/alternative origin point y, in absolute px - * @param {Function=} opt_onHide Optional callback for when the drop-down is hidden + * @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. + * @param {number} secondaryY Secondary/alternative origin point y, in absolute + * px. + * @param {Function=} opt_onHide Optional callback for when the drop-down is + * hidden. * @return {boolean} True if the menu rendered at the primary origin point. * @package */ @@ -305,21 +346,16 @@ Blockly.DropDownDiv.show = function(owner, rtl, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) { Blockly.DropDownDiv.owner_ = owner; Blockly.DropDownDiv.onHide_ = opt_onHide || null; - var metrics = Blockly.DropDownDiv.getPositionMetrics(primaryX, primaryY, - secondaryX, secondaryY); - // Update arrow CSS. - if (metrics.arrowVisible) { - Blockly.DropDownDiv.arrow_.style.display = ''; - Blockly.DropDownDiv.arrow_.style.transform = 'translate(' + - metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)'; - Blockly.DropDownDiv.arrow_.setAttribute('class', metrics.arrowAtTop ? - 'blocklyDropDownArrow arrowTop' : 'blocklyDropDownArrow arrowBottom'); - } else { - Blockly.DropDownDiv.arrow_.style.display = 'none'; - } - // Set direction. - Blockly.DropDownDiv.DIV_.style.direction = rtl ? 'rtl' : 'ltr'; + var div = Blockly.DropDownDiv.DIV_; + div.style.direction = rtl ? 'rtl' : 'ltr'; + + Blockly.DropDownDiv.rendererClassName_ = + Blockly.getMainWorkspace().getRenderer().name + '-renderer'; + Blockly.DropDownDiv.themeClassName_ = + Blockly.getMainWorkspace().getTheme().name + '-theme'; + Blockly.utils.dom.addClass(div, Blockly.DropDownDiv.rendererClassName_); + Blockly.utils.dom.addClass(div, Blockly.DropDownDiv.themeClassName_); // When we change `translate` multiple times in close succession, // Chrome may choose to wait and apply them all at once. @@ -330,10 +366,8 @@ Blockly.DropDownDiv.show = function(owner, rtl, primaryX, primaryY, // Using both `left`, `top` for the initial translation and then `translate` // for the animated transition to final X, Y is a workaround. - Blockly.DropDownDiv.positionInternal_( - metrics.initialX, metrics.initialY, - metrics.finalX, metrics.finalY); - return metrics.arrowAtTop; + return Blockly.DropDownDiv.positionInternal_( + primaryX, primaryY, secondaryX, secondaryY); }; /** @@ -343,14 +377,16 @@ Blockly.DropDownDiv.show = function(owner, rtl, primaryX, primaryY, * @private */ Blockly.DropDownDiv.getBoundsInfo_ = function() { - var boundPosition = Blockly.DropDownDiv.boundsElement_.getBoundingClientRect(); - var boundSize = Blockly.utils.style.getSize(Blockly.DropDownDiv.boundsElement_); + var boundPosition = Blockly.utils.style.getPageOffset( + /** @type {!Element} */ (Blockly.DropDownDiv.boundsElement_)); + var boundSize = Blockly.utils.style.getSize( + /** @type {!Element} */ (Blockly.DropDownDiv.boundsElement_)); return { - left: boundPosition.left, - right: boundPosition.left + boundSize.width, - top: boundPosition.top, - bottom: boundPosition.top + boundSize.height, + left: boundPosition.x, + right: boundPosition.x + boundSize.width, + top: boundPosition.y, + bottom: boundPosition.y + boundSize.height, width: boundSize.width, height: boundSize.height }; @@ -367,8 +403,9 @@ Blockly.DropDownDiv.getBoundsInfo_ = function() { * in absolute px. * @return {Object} Various final metrics, including rendered positions * for drop-down and arrow. + * @private */ -Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, +Blockly.DropDownDiv.getPositionMetrics_ = function(primaryX, primaryY, secondaryX, secondaryY) { var boundsInfo = Blockly.DropDownDiv.getBoundsInfo_(); var divSize = Blockly.utils.style.getSize( @@ -376,27 +413,27 @@ Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, // Can we fit in-bounds below the target? if (primaryY + divSize.height < boundsInfo.bottom) { - return Blockly.DropDownDiv.getPositionBelowMetrics( + return Blockly.DropDownDiv.getPositionBelowMetrics_( primaryX, primaryY, boundsInfo, divSize); } // Can we fit in-bounds above the target? if (secondaryY - divSize.height > boundsInfo.top) { - return Blockly.DropDownDiv.getPositionAboveMetrics( + return Blockly.DropDownDiv.getPositionAboveMetrics_( secondaryX, secondaryY, boundsInfo, divSize); } // Can we fit outside the workspace bounds (but inside the window) below? if (primaryY + divSize.height < document.documentElement.clientHeight) { - return Blockly.DropDownDiv.getPositionBelowMetrics( + return Blockly.DropDownDiv.getPositionBelowMetrics_( primaryX, primaryY, boundsInfo, divSize); } // Can we fit outside the workspace bounds (but inside the window) above? if (secondaryY - divSize.height > document.documentElement.clientTop) { - return Blockly.DropDownDiv.getPositionAboveMetrics( + return Blockly.DropDownDiv.getPositionAboveMetrics_( secondaryX, secondaryY, boundsInfo, divSize); } // Last resort, render at top of page. - return Blockly.DropDownDiv.getPositionTopOfPageMetrics( + return Blockly.DropDownDiv.getPositionTopOfPageMetrics_( primaryX, boundsInfo, divSize); }; @@ -410,8 +447,9 @@ Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, * of the DropDownDiv (width & height). * @return {Object} Various final metrics, including rendered positions * for drop-down and arrow. + * @private */ -Blockly.DropDownDiv.getPositionBelowMetrics = function( +Blockly.DropDownDiv.getPositionBelowMetrics_ = function( primaryX, primaryY, boundsInfo, divSize) { var xCoords = Blockly.DropDownDiv.getPositionX( @@ -445,8 +483,9 @@ Blockly.DropDownDiv.getPositionBelowMetrics = function( * of the DropDownDiv (width & height). * @return {Object} Various final metrics, including rendered positions * for drop-down and arrow. + * @private */ -Blockly.DropDownDiv.getPositionAboveMetrics = function( +Blockly.DropDownDiv.getPositionAboveMetrics_ = function( secondaryX, secondaryY, boundsInfo, divSize) { var xCoords = Blockly.DropDownDiv.getPositionX( @@ -478,8 +517,9 @@ Blockly.DropDownDiv.getPositionAboveMetrics = function( * of the DropDownDiv (width & height). * @return {Object} Various final metrics, including rendered positions * for drop-down and arrow. + * @private */ -Blockly.DropDownDiv.getPositionTopOfPageMetrics = function( +Blockly.DropDownDiv.getPositionTopOfPageMetrics_ = function( sourceX, boundsInfo, divSize) { var xCoords = Blockly.DropDownDiv.getPositionX( @@ -506,6 +546,7 @@ Blockly.DropDownDiv.getPositionTopOfPageMetrics = function( * @param {number} divWidth The width of the div in px. * @return {{divX: number, arrowX: number}} An object containing metrics for * the x positions of the left side of the DropDownDiv and the arrow. + * @package */ Blockly.DropDownDiv.getPositionX = function( sourceX, boundsLeft, boundsRight, divWidth) { @@ -600,8 +641,8 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { div.style.top = ''; div.style.opacity = 0; div.style.display = 'none'; - div.style.backgroundColor = Blockly.DropDownDiv.DEFAULT_DROPDOWN_COLOR; - div.style.borderColor = Blockly.DropDownDiv.DEFAULT_DROPDOWN_BORDER_COLOR; + div.style.backgroundColor = ''; + div.style.borderColor = ''; if (Blockly.DropDownDiv.onHide_) { Blockly.DropDownDiv.onHide_(); @@ -609,25 +650,50 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { } Blockly.DropDownDiv.clearContent(); Blockly.DropDownDiv.owner_ = null; + + if (Blockly.DropDownDiv.rendererClassName_) { + Blockly.utils.dom.removeClass(div, Blockly.DropDownDiv.rendererClassName_); + Blockly.DropDownDiv.rendererClassName_ = null; + } + if (Blockly.DropDownDiv.themeClassName_) { + Blockly.utils.dom.removeClass(div, Blockly.DropDownDiv.themeClassName_); + Blockly.DropDownDiv.themeClassName_ = null; + } + Blockly.getMainWorkspace().markFocused(); }; /** * Set the dropdown div's position. - * @param {number} initialX Initial Horizontal location - * (window coordinates, not body). - * @param {number} initialY Initial Vertical location - * (window coordinates, not body). - * @param {number} finalX Final Horizontal location - * (window coordinates, not body). - * @param {number} finalY Final Vertical location - * (window coordinates, not body). + * @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. + * @param {number} secondaryY Secondary/alternative origin point y, + * in absolute px. + * @return {boolean} True if the menu rendered at the primary origin point. * @private */ -Blockly.DropDownDiv.positionInternal_ = function(initialX, initialY, finalX, finalY) { - initialX = Math.floor(initialX); - initialY = Math.floor(initialY); - finalX = Math.floor(finalX); - finalY = Math.floor(finalY); +Blockly.DropDownDiv.positionInternal_ = function( + primaryX, primaryY, secondaryX, secondaryY) { + var metrics = Blockly.DropDownDiv.getPositionMetrics_(primaryX, primaryY, + secondaryX, secondaryY); + + // Update arrow CSS. + if (metrics.arrowVisible) { + Blockly.DropDownDiv.arrow_.style.display = ''; + Blockly.DropDownDiv.arrow_.style.transform = 'translate(' + + metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)'; + Blockly.DropDownDiv.arrow_.setAttribute('class', metrics.arrowAtTop ? + 'blocklyDropDownArrow blocklyArrowTop' : + 'blocklyDropDownArrow blocklyArrowBottom'); + } else { + Blockly.DropDownDiv.arrow_.style.display = 'none'; + } + + var initialX = Math.floor(metrics.initialX); + var initialY = Math.floor(metrics.initialY); + var finalX = Math.floor(metrics.finalX); + var finalY = Math.floor(metrics.finalY); var div = Blockly.DropDownDiv.DIV_; // First apply initial translation. @@ -643,42 +709,35 @@ Blockly.DropDownDiv.positionInternal_ = function(initialX, initialY, finalX, fin var dx = finalX - initialX; var dy = finalY - initialY; div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)'; + + return metrics.arrowAtTop; }; /** - * Repositions the dropdownDiv on window resize. If it doesn't know how to - * calculate the new position, it will just hide it instead. + * Repositions the dropdownDiv on window resize. If it doesn't know how to + * calculate the new position, it will just hide it instead. + * @package */ Blockly.DropDownDiv.repositionForWindowResize = function() { // This condition mainly catches the dropdown div when it is being used as a // dropdown. It is important not to close it in this case because on Android, // when a field is focused, the soft keyboard opens triggering a window resize - // event and we want the dropdown div to stick around so users can type into it. + // event and we want the dropdown div to stick around so users can type into + // it. if (Blockly.DropDownDiv.owner_) { + var field = /** @type {!Blockly.Field} */ (Blockly.DropDownDiv.owner_); var block = Blockly.DropDownDiv.owner_.getSourceBlock(); - var scale = block.workspace.scale; - var bBox = { - width: Blockly.DropDownDiv.positionToField_ ? - Blockly.DropDownDiv.owner_.size_.width : block.width, - height: Blockly.DropDownDiv.positionToField_ ? - Blockly.DropDownDiv.owner_.size_.height : block.height - }; - bBox.width *= scale; - bBox.height *= scale; - var position = Blockly.DropDownDiv.positionToField_ ? - Blockly.DropDownDiv.owner_.fieldGroup_.getBoundingClientRect() : - block.getSvgRoot().getBoundingClientRect(); + var bBox = Blockly.DropDownDiv.positionToField_ ? + Blockly.DropDownDiv.getScaledBboxOfField_(field) : + Blockly.DropDownDiv.getScaledBboxOfBlock_(block); // If we can fit it, render below the block. - var primaryX = position.left + bBox.width / 2; - var primaryY = position.top + bBox.height; + var primaryX = bBox.left + (bBox.right - bBox.left) / 2; + var primaryY = bBox.bottom; // If we can't fit it, render above the entire parent block. var secondaryX = primaryX; - var secondaryY = position.top; - var metrics = Blockly.DropDownDiv.getPositionMetrics( - primaryX, primaryY, secondaryX, secondaryY); + var secondaryY = bBox.top; Blockly.DropDownDiv.positionInternal_( - metrics.initialX, metrics.initialY, - metrics.finalX, metrics.finalY); + primaryX, primaryY, secondaryX, secondaryY); } else { Blockly.DropDownDiv.hide(); } diff --git a/core/events.js b/core/events.js index a693cae04..7ebd9caa6 100644 --- a/core/events.js +++ b/core/events.js @@ -194,7 +194,7 @@ Blockly.Events.fire = function(event) { Blockly.Events.fireNow_ = function() { var queue = Blockly.Events.filter(Blockly.Events.FIRE_QUEUE_, true); Blockly.Events.FIRE_QUEUE_.length = 0; - for (var i = 0, event; event = queue[i]; i++) { + for (var i = 0, event; (event = queue[i]); i++) { if (!event.workspaceId) { continue; } @@ -220,7 +220,7 @@ Blockly.Events.filter = function(queueIn, forward) { var mergedQueue = []; var hash = Object.create(null); // Merge duplicates. - for (var i = 0, event; event = queue[i]; i++) { + for (var i = 0, event; (event = queue[i]); i++) { if (!event.isNull()) { var key = [event.type, event.blockId, event.workspaceId].join(' '); @@ -266,7 +266,7 @@ Blockly.Events.filter = function(queueIn, forward) { } // Move mutation events to the top of the queue. // Intentionally skip first event. - for (var i = 1, event; event = queue[i]; i++) { + for (var i = 1, event; (event = queue[i]); i++) { if (event.type == Blockly.Events.CHANGE && event.element == 'mutation') { queue.unshift(queue.splice(i, 1)[0]); @@ -280,7 +280,7 @@ Blockly.Events.filter = function(queueIn, forward) { * in the undo stack. Called by Blockly.Workspace.clearUndo. */ Blockly.Events.clearPendingUndo = function() { - for (var i = 0, event; event = Blockly.Events.FIRE_QUEUE_[i]; i++) { + for (var i = 0, event; (event = Blockly.Events.FIRE_QUEUE_[i]); i++) { event.recordUndo = false; } }; @@ -338,7 +338,7 @@ Blockly.Events.setGroup = function(state) { Blockly.Events.getDescendantIds = function(block) { var ids = []; var descendants = block.getDescendants(false); - for (var i = 0, descendant; descendant = descendants[i]; i++) { + for (var i = 0, descendant; (descendant = descendants[i]); i++) { ids[i] = descendant.id; } return ids; @@ -390,6 +390,9 @@ Blockly.Events.fromJson = function(json, workspace) { case Blockly.Events.COMMENT_DELETE: event = new Blockly.Events.CommentDelete(null); break; + case Blockly.Events.FINISHED_LOADING: + event = new Blockly.Events.FinishedLoading(workspace); + break; default: throw Error('Unknown event type.'); } @@ -417,7 +420,7 @@ Blockly.Events.disableOrphans = function(event) { var parent = block.getParent(); if (parent && parent.isEnabled()) { var children = block.getDescendants(false); - for (var i = 0, child; child = children[i]; i++) { + for (var i = 0, child; (child = children[i]); i++) { child.setEnabled(true); } } else if ((block.outputConnection || block.previousConnection) && diff --git a/core/extensions.js b/core/extensions.js index 3f43a3ca8..c410bb3e9 100644 --- a/core/extensions.js +++ b/core/extensions.js @@ -87,7 +87,7 @@ Blockly.Extensions.registerMixin = function(name, mixinObj) { * @param {!Object} mixinObj The values to mix in. * @param {(function())=} opt_helperFn An optional function to apply after * mixing in the object. - * @param {Array.=} opt_blockList A list of blocks to appear in the + * @param {!Array.=} opt_blockList A list of blocks to appear in the * flyout of the mutator dialog. * @throws {Error} if the mutation is invalid or can't be applied to the block. */ @@ -114,7 +114,7 @@ Blockly.Extensions.registerMutator = function(name, mixinObj, opt_helperFn, if (!Blockly.Mutator) { throw Error(errorPrefix + 'Missing require for Blockly.Mutator'); } - this.setMutator(new Blockly.Mutator(opt_blockList)); + this.setMutator(new Blockly.Mutator(opt_blockList || [])); } // Mixin the object. this.mixin(mixinObj); @@ -165,7 +165,8 @@ Blockly.Extensions.apply = function(name, block, isMutator) { var errorPrefix = 'Error after applying mutator "' + name + '": '; Blockly.Extensions.checkBlockHasMutatorProperties_(errorPrefix, block); } else { - if (!Blockly.Extensions.mutatorPropertiesMatch_(mutatorProperties, block)) { + if (!Blockly.Extensions.mutatorPropertiesMatch_( + /** @type {!Array.} */ (mutatorProperties), block)) { throw Error('Error when applying extension "' + name + '": ' + 'mutation properties changed when applying a non-mutator extension.'); } @@ -359,7 +360,7 @@ Blockly.Extensions.buildTooltipForDropdown = function(dropdownName, } this.setTooltip(function() { - var value = this.getFieldValue(dropdownName); + var value = String(this.getFieldValue(dropdownName)); var tooltip = lookupTable[value]; if (tooltip == null) { if (blockTypesChecked.indexOf(this.type) == -1) { diff --git a/core/field.js b/core/field.js index a9d7b919c..09507898d 100644 --- a/core/field.js +++ b/core/field.js @@ -31,9 +31,10 @@ goog.require('Blockly.Gesture'); goog.require('Blockly.utils'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.Size'); +goog.require('Blockly.utils.style'); goog.require('Blockly.utils.userAgent'); -goog.require('Blockly.utils.style'); +goog.requireType('Blockly.blockRendering.ConstantProvider'); /** @@ -94,46 +95,53 @@ Blockly.Field = function(value, opt_validator, opt_config) { */ this.markerSvg_ = null; + /** + * The rendered field's SVG group element. + * @type {SVGGElement} + * @protected + */ + this.fieldGroup_ = null; + + /** + * The rendered field's SVG border element. + * @type {SVGRectElement} + * @protected + */ + this.borderRect_ = null; + + /** + * The rendered field's SVG text element. + * @type {SVGTextElement} + * @protected + */ + this.textElement_ = null; + + /** + * The rendered field's text content element. + * @type {Text} + * @protected + */ + this.textContent_ = null; + + /** + * Mouse down event listener data. + * @type {?Blockly.EventData} + * @private + */ + this.mouseDownWrapper_ = null; + + /** + * Constants associated with the source block's renderer. + * @type {Blockly.blockRendering.ConstantProvider} + * @protected + */ + this.constants_ = null; + opt_config && this.configure_(opt_config); this.setValue(value); opt_validator && this.setValidator(opt_validator); }; -/** - * The default height of the border rect on any field. - * @type {number} - * @package - */ -Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT = 16; - -/** - * The default height of the text element on any field. - * @type {number} - * @package - */ -Blockly.Field.TEXT_DEFAULT_HEIGHT = 12.5; - -/** - * The padding added to the width by the border rect, if it exists. - * @type {number} - * @package - */ -Blockly.Field.X_PADDING = 10; - -/** - * The padding added to the height by the border rect, if it exists. - * @type {number} - * @package - */ -Blockly.Field.Y_PADDING = 10; - -/** - * The default offset between the left of the text element and the left of the - * border rect, if the border rect exists. - * @type {number} - */ -Blockly.Field.DEFAULT_TEXT_OFFSET = Blockly.Field.X_PADDING / 2; - /** * Name of field. Unique within each block. * Static labels are usually unnamed. @@ -178,7 +186,7 @@ Blockly.Field.prototype.visible_ = true; /** * The element the click handler is bound to. * @type {Element} - * @private + * @protected */ Blockly.Field.prototype.clickTarget_ = null; @@ -186,11 +194,23 @@ Blockly.Field.prototype.clickTarget_ = null; * A developer hook to override the returned text of this field. * Override if the text representation of the value of this field * is not just a string cast of its value. + * Return null to resort to a string cast. * @return {?string} Current text. Return null to resort to a string cast. * @protected */ Blockly.Field.prototype.getText_; +/** + * An optional method that can be defined to show an editor when the field is + * clicked. Blockly will automatically set the field as clickable if this + * method is defined. + * @param {Event=} opt_e Optional mouse event that triggered the field to open, + * or undefined if triggered programatically. + * @return {void} + * @protected + */ +Blockly.Field.prototype.showEditor_; + /** * Non-breaking space. * @const @@ -240,6 +260,9 @@ Blockly.Field.prototype.setSourceBlock = function(block) { throw Error('Field already bound to a block.'); } this.sourceBlock_ = block; + if (block.workspace.rendered) { + this.constants_ = block.workspace.getRenderer().getConstants(); + } }; /** @@ -260,11 +283,13 @@ Blockly.Field.prototype.init = function() { // Field has already been initialized once. return; } - this.fieldGroup_ = Blockly.utils.dom.createSvgElement('g', {}, null); + this.fieldGroup_ = /** @type {!SVGGElement} **/ + (Blockly.utils.dom.createSvgElement('g', {}, null)); if (!this.isVisible()) { this.fieldGroup_.style.display = 'none'; } - this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); + var sourceBlockSvg = /** @type {!Blockly.BlockSvg} **/ (this.sourceBlock_); + sourceBlockSvg.getSvgRoot().appendChild(this.fieldGroup_); this.initView(); this.updateEditable(); this.setTooltip(this.tooltip_); @@ -297,18 +322,20 @@ Blockly.Field.prototype.initModel = function() { */ Blockly.Field.prototype.createBorderRect_ = function() { this.size_.height = - Math.max(this.size_.height, Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT); + Math.max(this.size_.height, this.constants_.FIELD_BORDER_RECT_HEIGHT); this.size_.width = - Math.max(this.size_.width, Blockly.Field.X_PADDING); - this.borderRect_ = Blockly.utils.dom.createSvgElement('rect', - { - 'rx': 4, - 'ry': 4, - 'x': 0, - 'y': 0, - 'height': this.size_.height, - 'width': this.size_.width - }, this.fieldGroup_); + Math.max(this.size_.width, this.constants_.FIELD_BORDER_RECT_X_PADDING * 2); + this.borderRect_ = /** @type {!SVGRectElement} **/ + (Blockly.utils.dom.createSvgElement('rect', + { + 'rx': this.constants_.FIELD_BORDER_RECT_RADIUS, + 'ry': this.constants_.FIELD_BORDER_RECT_RADIUS, + 'x': 0, + 'y': 0, + 'height': this.size_.height, + 'width': this.size_.width, + 'class': 'blocklyFieldRect' + }, this.fieldGroup_)); }; /** @@ -318,14 +345,26 @@ Blockly.Field.prototype.createBorderRect_ = function() { * @protected */ Blockly.Field.prototype.createTextElement_ = function() { - var xOffset = this.borderRect_ ? Blockly.Field.DEFAULT_TEXT_OFFSET : 0; - this.textElement_ = Blockly.utils.dom.createSvgElement('text', - { - 'class': 'blocklyText', - // The y position is the baseline of the text. - 'y': Blockly.Field.TEXT_DEFAULT_HEIGHT, - 'x': xOffset - }, this.fieldGroup_); + var xOffset = this.borderRect_ ? + this.constants_.FIELD_BORDER_RECT_X_PADDING : 0; + var baselineCenter = this.constants_.FIELD_TEXT_BASELINE_CENTER; + var baselineY = this.constants_.FIELD_TEXT_BASELINE_Y; + this.size_.height = Math.max(this.size_.height, baselineCenter ? + this.constants_.FIELD_TEXT_HEIGHT : baselineY); + if (this.size_.height > this.constants_.FIELD_TEXT_HEIGHT) { + baselineY += (this.size_.height - baselineY) / 2; + } + this.textElement_ = /** @type {!SVGTextElement} **/ + (Blockly.utils.dom.createSvgElement('text', + { + 'class': 'blocklyText', + 'y': baselineCenter ? this.size_.height / 2 : baselineY, + 'dy': this.constants_.FIELD_TEXT_Y_OFFSET, + 'x': xOffset + }, this.fieldGroup_)); + if (baselineCenter) { + this.textElement_.setAttribute('dominant-baseline', 'central'); + } this.textContent_ = document.createTextNode(''); this.textElement_.appendChild(this.textContent_); }; @@ -386,7 +425,7 @@ Blockly.Field.prototype.dispose = function() { * Add or remove the UI indicating if this field is editable or not. */ Blockly.Field.prototype.updateEditable = function() { - var group = this.getClickTarget_(); + var group = this.fieldGroup_; if (!this.EDITABLE || !group) { return; } @@ -534,18 +573,18 @@ Blockly.Field.prototype.callValidator = function(text) { /** * Gets the group element for this editable field. * Used for measuring the size and for positioning. - * @return {!SVGElement} The group element. + * @return {!SVGGElement} The group element. */ Blockly.Field.prototype.getSvgRoot = function() { - return /** @type {!SVGElement} */ (this.fieldGroup_); + return /** @type {!SVGGElement} */ (this.fieldGroup_); }; /** * Updates the field to match the colour/style of the block. Should only be - * called by BlockSvg.updateColour(). + * called by BlockSvg.applyColour(). * @package */ -Blockly.Field.prototype.updateColour = function() { +Blockly.Field.prototype.applyColour = function() { // Non-abstract sub-classes may wish to implement this. See FieldDropdown. }; @@ -563,6 +602,18 @@ Blockly.Field.prototype.render_ = function() { } }; +/** + * Show an editor when the field is clicked only if the field is clickable. + * @param {Event=} opt_e Optional mouse event that triggered the field to open, + * or undefined if triggered programatically. + * @package + */ +Blockly.Field.prototype.showEditor = function(opt_e) { + if (this.isClickable()) { + this.showEditor_(opt_e); + } +}; + /** * Updates the width of the field. Redirects to updateSize_(). * @deprecated May 2019 Use Blockly.Field.updateSize_() to force an update @@ -582,10 +633,14 @@ Blockly.Field.prototype.updateWidth = function() { * @protected */ Blockly.Field.prototype.updateSize_ = function() { - var textWidth = Blockly.utils.dom.getTextWidth(this.textElement_); + var textWidth = Blockly.utils.dom.getFastTextWidth( + /** @type {!SVGTextElement} */ (this.textElement_), + this.constants_.FIELD_TEXT_FONTSIZE, + this.constants_.FIELD_TEXT_FONTWEIGHT, + this.constants_.FIELD_TEXT_FONTFAMILY); var totalWidth = textWidth; if (this.borderRect_) { - totalWidth += Blockly.Field.X_PADDING; + totalWidth += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2; this.borderRect_.setAttribute('width', totalWidth); } this.size_.width = totalWidth; @@ -620,13 +675,38 @@ Blockly.Field.prototype.getSize = function() { * scaling. * @return {!Object} An object with top, bottom, left, and right in pixels * relative to the top left corner of the page (window coordinates). - * @protected + * @package */ -Blockly.Field.prototype.getScaledBBox_ = function() { - var bBox = this.borderRect_.getBBox(); - var scaledHeight = bBox.height * this.sourceBlock_.workspace.scale; - var scaledWidth = bBox.width * this.sourceBlock_.workspace.scale; - var xy = this.getAbsoluteXY_(); +Blockly.Field.prototype.getScaledBBox = function() { + if (!this.borderRect_) { + // Browsers are inconsistent in what they return for a bounding box. + // - Webkit / Blink: fill-box / object bounding box + // - Gecko / Triden / EdgeHTML: stroke-box + var bBox = this.sourceBlock_.getHeightWidth(); + var scale = this.sourceBlock_.workspace.scale; + var xy = this.getAbsoluteXY_(); + var scaledWidth = bBox.width * scale; + var scaledHeight = bBox.height * scale; + + if (Blockly.utils.userAgent.GECKO) { + xy.x += 1.5 * scale; + xy.y += 1.5 * scale; + scaledWidth += 1 * scale; + scaledHeight += 1 * scale; + } else { + if (!Blockly.utils.userAgent.EDGE && !Blockly.utils.userAgent.IE) { + xy.x -= 0.5 * scale; + xy.y -= 0.5 * scale; + } + scaledWidth += 1 * scale; + scaledHeight += 1 * scale; + } + } else { + var bBox = this.borderRect_.getBoundingClientRect(); + var xy = Blockly.utils.style.getPageOffset(this.borderRect_); + var scaledWidth = bBox.width; + var scaledHeight = bBox.height; + } return { top: xy.y, bottom: xy.y + scaledHeight, @@ -868,7 +948,7 @@ Blockly.Field.prototype.setTooltip = function(newTip) { * to the SVG root of the field. When this element is * clicked on an editable field, the editor will open. * @return {!Element} Element to bind click handler to. - * @private + * @protected */ Blockly.Field.prototype.getClickTarget_ = function() { return this.clickTarget_ || this.getSvgRoot(); @@ -878,10 +958,11 @@ Blockly.Field.prototype.getClickTarget_ = function() { * Return the absolute coordinates of the top-left corner of this field. * The origin (0,0) is the top-left corner of the page body. * @return {!Blockly.utils.Coordinate} Object with .x and .y properties. - * @private + * @protected */ Blockly.Field.prototype.getAbsoluteXY_ = function() { - return Blockly.utils.style.getPageOffset(this.borderRect_); + return Blockly.utils.style.getPageOffset( + /** @type {!SVGRectElement} */ (this.getClickTarget_())); }; /** diff --git a/core/field_angle.js b/core/field_angle.js index a8f9fb869..131adce28 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -83,6 +83,39 @@ Blockly.FieldAngle = function(opt_value, opt_validator, opt_config) { Blockly.FieldAngle.superClass_.constructor.call( this, opt_value || 0, opt_validator, opt_config); + + /** + * The angle picker's gauge path depending on the value. + * @type {SVGElement} + */ + this.gauge_ = null; + + /** + * The angle picker's line drawn representing the value's angle. + * @type {SVGElement} + */ + this.line_ = null; + + /** + * Wrapper click event data. + * @type {?Blockly.EventData} + * @private + */ + this.clickWrapper_ = null; + + /** + * Surface click event data. + * @type {?Blockly.EventData} + * @private + */ + this.clickSurfaceWrapper_ = null; + + /** + * Surface mouse move event data. + * @type {?Blockly.EventData} + * @private + */ + this.moveSurfaceWrapper_ = null; }; Blockly.utils.object.inherits(Blockly.FieldAngle, Blockly.FieldTextInput); @@ -173,6 +206,7 @@ Blockly.FieldAngle.prototype.configure_ = function(config) { this.clockwise_ = clockwise; } + // If these are passed as null then we should leave them on the default. var offset = config['offset']; if (offset != null) { offset = Number(offset); @@ -222,22 +256,23 @@ 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 programatically. * @private */ -Blockly.FieldAngle.prototype.showEditor_ = function() { +Blockly.FieldAngle.prototype.showEditor_ = function(opt_e) { // Mobile browsers have issues with in-line textareas (focus & keyboards). var noFocus = Blockly.utils.userAgent.MOBILE || Blockly.utils.userAgent.ANDROID || Blockly.utils.userAgent.IPAD; - Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); + Blockly.FieldAngle.superClass_.showEditor_.call(this, opt_e, noFocus); var editor = this.dropdownCreate_(); Blockly.DropDownDiv.getContentDiv().appendChild(editor); - var border = this.sourceBlock_.getColourBorder(); - border = border.colourBorder || border.colourLight; - Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), border); + Blockly.DropDownDiv.setColour(this.sourceBlock_.style.colourPrimary, + this.sourceBlock_.style.colourTertiary); Blockly.DropDownDiv.showPositionedByField( this, this.dropdownDispose_.bind(this)); @@ -297,20 +332,33 @@ Blockly.FieldAngle.prototype.dropdownCreate_ = function() { // a click handler on the drag surface to update the value if the surface // is clicked. this.clickSurfaceWrapper_ = - Blockly.bindEventWithChecks_(circle, 'click', this, this.onMouseMove, true, true); + Blockly.bindEventWithChecks_(circle, 'click', this, this.onMouseMove_, + true, true); this.moveSurfaceWrapper_ = - Blockly.bindEventWithChecks_(circle, 'mousemove', this, this.onMouseMove, true, true); + Blockly.bindEventWithChecks_(circle, 'mousemove', this, this.onMouseMove_, + true, true); return svg; }; /** - * Dispose of events belonging to the angle editor. + * Disposes of events and dom-references belonging to the angle editor. * @private */ Blockly.FieldAngle.prototype.dropdownDispose_ = function() { - Blockly.unbindEvent_(this.clickWrapper_); - Blockly.unbindEvent_(this.clickSurfaceWrapper_); - Blockly.unbindEvent_(this.moveSurfaceWrapper_); + if (this.clickWrapper_) { + Blockly.unbindEvent_(this.clickWrapper_); + this.clickWrapper_ = null; + } + if (this.clickSurfaceWrapper_) { + Blockly.unbindEvent_(this.clickSurfaceWrapper_); + this.clickSurfaceWrapper_ = null; + } + if (this.moveSurfaceWrapper_) { + Blockly.unbindEvent_(this.moveSurfaceWrapper_); + this.moveSurfaceWrapper_ = null; + } + this.gauge_ = null; + this.line_ = null; }; /** @@ -325,8 +373,9 @@ Blockly.FieldAngle.prototype.hide_ = function() { /** * Set the angle to match the mouse's position. * @param {!Event} e Mouse move event. + * @protected */ -Blockly.FieldAngle.prototype.onMouseMove = function(e) { +Blockly.FieldAngle.prototype.onMouseMove_ = function(e) { // Calculate angle. var bBox = this.gauge_.ownerSVGElement.getBoundingClientRect(); var dx = e.clientX - bBox.left - Blockly.FieldAngle.HALF; diff --git a/core/field_checkbox.js b/core/field_checkbox.js index e3c83fc78..ea417927f 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -60,9 +60,6 @@ Blockly.FieldCheckbox = function(opt_value, opt_validator, opt_config) { } Blockly.FieldCheckbox.superClass_.constructor.call( this, opt_value, opt_validator, opt_config); - - this.size_.width = Blockly.FieldCheckbox.WIDTH; - }; Blockly.utils.object.inherits(Blockly.FieldCheckbox, Blockly.Field); @@ -77,13 +74,6 @@ Blockly.FieldCheckbox.fromJson = function(options) { return new Blockly.FieldCheckbox(options['checked'], undefined, options); }; -/** - * The width of a checkbox field. - * @type {number} - * @const - */ -Blockly.FieldCheckbox.WIDTH = 15; - /** * Default character for the checkmark. * @type {string} @@ -91,20 +81,6 @@ Blockly.FieldCheckbox.WIDTH = 15; */ Blockly.FieldCheckbox.CHECK_CHAR = '\u2713'; -/** - * Used to correctly position the check mark. - * @type {number} - * @const - */ -Blockly.FieldCheckbox.CHECK_X_OFFSET = Blockly.Field.DEFAULT_TEXT_OFFSET - 3; - -/** - * Used to correctly position the check mark. - * @type {number} - * @const - */ -Blockly.FieldCheckbox.CHECK_Y_OFFSET = 14; - /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. @@ -143,10 +119,12 @@ Blockly.FieldCheckbox.prototype.configure_ = function(config) { * @package */ Blockly.FieldCheckbox.prototype.initView = function() { + this.size_.width = this.constants_.FIELD_CHECKBOX_DEFAULT_WIDTH; Blockly.FieldCheckbox.superClass_.initView.call(this); - this.textElement_.setAttribute('x', Blockly.FieldCheckbox.CHECK_X_OFFSET); - this.textElement_.setAttribute('y', Blockly.FieldCheckbox.CHECK_Y_OFFSET); + this.textElement_.setAttribute('x', this.constants_.FIELD_CHECKBOX_X_OFFSET); + this.textElement_.setAttribute('y', this.constants_.FIELD_CHECKBOX_Y_OFFSET); + this.textElement_.removeAttribute('dominant-baseline'); Blockly.utils.dom.addClass(this.textElement_, 'blocklyCheckbox'); this.textContent_.nodeValue = diff --git a/core/field_colour.js b/core/field_colour.js index 88c9891d4..1909f874c 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -58,13 +58,53 @@ Blockly.FieldColour = function(opt_value, opt_validator, opt_config) { opt_validator, opt_config); /** - * The size of the area rendered by the field. - * @type {Blockly.utils.Size} - * @protected - * @override + * The field's colour picker element. + * @type {Element} + * @private */ - this.size_ = new Blockly.utils.Size(Blockly.FieldColour.DEFAULT_WIDTH, - Blockly.FieldColour.DEFAULT_HEIGHT); + this.picker_ = null; + + /** + * Index of the currently highlighted element. + * @type {?number} + * @private + */ + this.highlightedIndex_ = null; + + /** + * Mouse click event data. + * @type {?Blockly.EventData} + * @private + */ + this.onClickWrapper_ = null; + + /** + * Mouse move event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseMoveWrapper_ = null; + + /** + * Mouse enter event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseEnterWrapper_ = null; + + /** + * Mouse leave event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseLeaveWrapper_ = null; + + /** + * Key down event data. + * @type {?Blockly.EventData} + * @private + */ + this.onKeyDownWrapper_ = null; }; Blockly.utils.object.inherits(Blockly.FieldColour, Blockly.Field); @@ -79,22 +119,6 @@ Blockly.FieldColour.fromJson = function(options) { return new Blockly.FieldColour(options['colour'], undefined, options); }; -/** - * Default width of a colour field. - * @type {number} - * @private - * @const - */ -Blockly.FieldColour.DEFAULT_WIDTH = 26; - -/** - * Default height of a colour field. - * @type {number} - * @private - * @const - */ -Blockly.FieldColour.DEFAULT_HEIGHT = Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT; - /** * Serializable fields are saved by the XML renderer, non-serializable fields * are not. Editable fields should also be serializable. @@ -159,9 +183,29 @@ Blockly.FieldColour.prototype.configure_ = function(config) { * @package */ Blockly.FieldColour.prototype.initView = function() { - this.createBorderRect_(); - this.borderRect_.style['fillOpacity'] = 1; - this.borderRect_.style.fill = this.value_; + this.size_ = new Blockly.utils.Size( + this.constants_.FIELD_COLOUR_DEFAULT_WIDTH, + this.constants_.FIELD_COLOUR_DEFAULT_HEIGHT); + if (!this.constants_.FIELD_COLOUR_FULL_BLOCK) { + this.createBorderRect_(); + this.borderRect_.style['fillOpacity'] = '1'; + } else { + this.clickTarget_ = this.sourceBlock_.getSvgRoot(); + } +}; + +/** + * @override + */ +Blockly.FieldColour.prototype.applyColour = function() { + if (!this.constants_.FIELD_COLOUR_FULL_BLOCK) { + if (this.borderRect_) { + this.borderRect_.style.fill = this.getValue(); + } + } else { + this.sourceBlock_.pathObject.svgPath.setAttribute('fill', this.getValue()); + this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff'); + } }; /** @@ -187,6 +231,9 @@ Blockly.FieldColour.prototype.doValueUpdate_ = function(newValue) { this.value_ = newValue; if (this.borderRect_) { this.borderRect_.style.fill = newValue; + } else if (this.sourceBlock_) { + this.sourceBlock_.pathObject.svgPath.setAttribute('fill', newValue); + this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff'); } }; @@ -285,7 +332,7 @@ Blockly.FieldColour.prototype.showEditor_ = function() { this, this.dropdownDispose_.bind(this)); // Focus so we can start receiving keyboard events. - this.picker_.focus(); + this.picker_.focus({preventScroll:true}); }; /** @@ -415,7 +462,7 @@ Blockly.FieldColour.prototype.moveHighlightBy_ = function(dx, dy) { } // Move the highlight to the new coordinates. - var cell = this.picker_.childNodes[y].childNodes[x]; + var cell = /** @type {!Element} */ (this.picker_.childNodes[y].childNodes[x]); var index = (y * columns) + x; this.setHighlightedCell_(cell, index); }; @@ -427,9 +474,9 @@ Blockly.FieldColour.prototype.moveHighlightBy_ = function(dx, dy) { */ Blockly.FieldColour.prototype.onMouseMove_ = function(e) { var cell = /** @type {!Element} */ (e.target); - var index = cell && cell.getAttribute('data-index'); + var index = cell && Number(cell.getAttribute('data-index')); if (index !== null && index !== this.highlightedIndex_) { - this.setHighlightedCell_(cell, Number(index)); + this.setHighlightedCell_(cell, index); } }; @@ -438,7 +485,7 @@ Blockly.FieldColour.prototype.onMouseMove_ = function(e) { * @private */ Blockly.FieldColour.prototype.onMouseEnter_ = function() { - this.picker_.focus(); + this.picker_.focus({preventScroll:true}); }; /** @@ -456,7 +503,7 @@ Blockly.FieldColour.prototype.onMouseLeave_ = function() { /** * Returns the currently highlighted item (if any). - * @return {Element} Highlighted item (null if none). + * @return {HTMLElement} Highlighted item (null if none). * @private */ Blockly.FieldColour.prototype.getHighlighted_ = function() { @@ -467,7 +514,7 @@ Blockly.FieldColour.prototype.getHighlighted_ = function() { if (!row) { return null; } - var col = row.childNodes[x]; + var col = /** @type {HTMLElement} */ (row.childNodes[x]); return col; }; @@ -489,7 +536,7 @@ Blockly.FieldColour.prototype.setHighlightedCell_ = function(cell, index) { this.highlightedIndex_ = index; // Update accessibility roles. - Blockly.utils.aria.setState(this.picker_, + Blockly.utils.aria.setState(/** @type {!Element} */ (this.picker_), Blockly.utils.aria.State.ACTIVEDESCENDANT, cell.getAttribute('id')); }; @@ -508,13 +555,12 @@ Blockly.FieldColour.prototype.dropdownCreate_ = function() { table.className = 'blocklyColourTable'; table.tabIndex = 0; table.dir = 'ltr'; - Blockly.utils.aria.setRole(table, - Blockly.utils.aria.Role.GRID); - Blockly.utils.aria.setState(table, - Blockly.utils.aria.State.EXPANDED, true); - Blockly.utils.aria.setState(table, 'rowcount', + Blockly.utils.aria.setRole(table, Blockly.utils.aria.Role.GRID); + Blockly.utils.aria.setState(table, Blockly.utils.aria.State.EXPANDED, true); + Blockly.utils.aria.setState(table, Blockly.utils.aria.State.ROWCOUNT, Math.floor(colours.length / columns)); - Blockly.utils.aria.setState(table, 'colcount', columns); + Blockly.utils.aria.setState(table, Blockly.utils.aria.State.COLCOUNT, + columns); var row; for (var i = 0; i < colours.length; i++) { if (i % columns == 0) { @@ -556,16 +602,32 @@ Blockly.FieldColour.prototype.dropdownCreate_ = function() { }; /** - * Dispose of events belonging to the colour editor. + * Disposes of events and dom-references belonging to the colour editor. * @private */ Blockly.FieldColour.prototype.dropdownDispose_ = function() { - Blockly.unbindEvent_(this.onClickWrapper_); - Blockly.unbindEvent_(this.onMouseMoveWrapper_); - Blockly.unbindEvent_(this.onMouseEnterWrapper_); - Blockly.unbindEvent_(this.onMouseLeaveWrapper_); - Blockly.unbindEvent_(this.onKeyDownWrapper_); + if (this.onClickWrapper_) { + Blockly.unbindEvent_(this.onClickWrapper_); + this.onClickWrapper_ = null; + } + if (this.onMouseMoveWrapper_) { + Blockly.unbindEvent_(this.onMouseMoveWrapper_); + this.onMouseMoveWrapper_ = null; + } + if (this.onMouseEnterWrapper_) { + Blockly.unbindEvent_(this.onMouseEnterWrapper_); + this.onMouseEnterWrapper_ = null; + } + if (this.onMouseLeaveWrapper_) { + Blockly.unbindEvent_(this.onMouseLeaveWrapper_); + this.onMouseLeaveWrapper_ = null; + } + if (this.onKeyDownWrapper_) { + Blockly.unbindEvent_(this.onKeyDownWrapper_); + this.onKeyDownWrapper_ = null; + } this.picker_ = null; + this.highlightedIndex_ = null; }; /** diff --git a/core/field_date.js b/core/field_date.js index ebbfbc562..a39ca849e 100644 --- a/core/field_date.js +++ b/core/field_date.js @@ -129,9 +129,9 @@ Blockly.FieldDate.prototype.render_ = function() { * Updates the field's colours to match those of the block. * @package */ -Blockly.FieldDate.prototype.updateColour = function() { - this.todayColour_ = this.sourceBlock_.getColour(); - this.selectedColour_ = this.sourceBlock_.getColourShadow(); +Blockly.FieldDate.prototype.applyColour = function() { + this.todayColour_ = this.sourceBlock_.style.colourPrimary; + this.selectedColour_ = this.sourceBlock_.style.colourSecondary; this.updateEditor_(); }; diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 44be889fa..4a42c3429 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -33,6 +33,8 @@ goog.require('Blockly.Menu'); goog.require('Blockly.MenuItem'); goog.require('Blockly.navigation'); goog.require('Blockly.utils'); +goog.require('Blockly.utils.aria'); +goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.Size'); @@ -76,27 +78,19 @@ Blockly.FieldDropdown = function(menuGenerator, opt_validator, opt_config) { */ this.generatedOptions_ = null; + this.trimOptions_(); + /** - * The currently selected index. The field is initialized with the + * The currently selected option. The field is initialized with the * first option selected. - * @type {number} + * @type {!Object} * @private */ - this.selectedIndex_ = 0; - - this.trimOptions_(); - var firstTuple = this.getOptions(false)[0]; + this.selectedOption_ = this.getOptions(false)[0]; // Call parent's constructor. Blockly.FieldDropdown.superClass_.constructor.call( - this, firstTuple[1], opt_validator, opt_config); - - /** - * SVG image element if currently selected option is an image, or null. - * @type {SVGElement} - * @private - */ - this.imageElement_ = null; + this, this.selectedOption_[1], opt_validator, opt_config); /** * A reference to the currently selected menu item. @@ -104,6 +98,34 @@ Blockly.FieldDropdown = function(menuGenerator, opt_validator, opt_config) { * @private */ this.selectedMenuItem_ = null; + + /** + * The dropdown menu. + * @type {Blockly.Menu} + * @private + */ + this.menu_ = null; + + /** + * SVG image element if currently selected option is an image, or null. + * @type {SVGImageElement} + * @private + */ + this.imageElement_ = null; + + /** + * Tspan based arrow element. + * @type {SVGTSpanElement} + * @private + */ + this.arrow_ = null; + + /** + * SVG based arrow element. + * @type {SVGElement} + * @private + */ + this.svgArrow_ = null; }; Blockly.utils.object.inherits(Blockly.FieldDropdown, Blockly.Field); @@ -180,14 +202,45 @@ Blockly.FieldDropdown.prototype.CURSOR = 'default'; * @package */ Blockly.FieldDropdown.prototype.initView = function() { - Blockly.FieldDropdown.superClass_.initView.call(this); + if (this.shouldAddBorderRect_()) { + this.createBorderRect_(); + } else { + this.clickTarget_ = this.sourceBlock_.getSvgRoot(); + } + this.createTextElement_(); - this.imageElement_ = Blockly.utils.dom.createSvgElement( 'image', - { - 'y': Blockly.FieldDropdown.IMAGE_Y_OFFSET - }, this.fieldGroup_); + this.imageElement_ = /** @type {!SVGImageElement} */ + (Blockly.utils.dom.createSvgElement('image', {}, this.fieldGroup_)); - this.arrow_ = Blockly.utils.dom.createSvgElement('tspan', {}, this.textElement_); + if (this.constants_.FIELD_DROPDOWN_SVG_ARROW) { + this.createSVGArrow_(); + } else { + this.createTextArrow_(); + } + + if (this.borderRect_) { + Blockly.utils.dom.addClass(this.borderRect_, 'blocklyDropdownRect'); + } +}; + +/** + * Whether or not the dropdown should add a border rect. + * @return {boolean} True if the dropdown field should add a border rect. + * @protected + */ +Blockly.FieldDropdown.prototype.shouldAddBorderRect_ = function() { + return !this.constants_.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW || + (this.constants_.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW && + !this.sourceBlock_.isShadow()); +}; + +/** + * Create a tspan based arrow. + * @protected + */ +Blockly.FieldDropdown.prototype.createTextArrow_ = function() { + this.arrow_ = /** @type {!SVGTSpanElement} */ + (Blockly.utils.dom.createSvgElement('tspan', {}, this.textElement_)); this.arrow_.appendChild(document.createTextNode( this.sourceBlock_.RTL ? Blockly.FieldDropdown.ARROW_CHAR + ' ' : @@ -199,17 +252,48 @@ Blockly.FieldDropdown.prototype.initView = function() { } }; +/** + * Create an SVG based arrow. + * @protected + */ +Blockly.FieldDropdown.prototype.createSVGArrow_ = function() { + this.svgArrow_ = Blockly.utils.dom.createSvgElement('image', { + 'height': this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px', + 'width': this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px' + }, this.fieldGroup_); + this.svgArrow_.setAttributeNS(Blockly.utils.dom.XLINK_NS, 'xlink:href', + this.constants_.FIELD_DROPDOWN_SVG_ARROW_DATAURI); +}; + /** * Create a dropdown menu under the text. + * @param {Event=} opt_e Optional mouse event that triggered the field to open, + * or undefined if triggered programatically. * @private */ -Blockly.FieldDropdown.prototype.showEditor_ = function() { +Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) { this.menu_ = this.dropdownCreate_(); + if (opt_e && typeof opt_e.clientX === 'number') { + this.menu_.openingCoords = + new Blockly.utils.Coordinate(opt_e.clientX, opt_e.clientY); + } else { + this.menu_.openingCoords = null; + } // Element gets created in render. this.menu_.render(Blockly.DropDownDiv.getContentDiv()); Blockly.utils.dom.addClass( /** @type {!Element} */ (this.menu_.getElement()), 'blocklyDropdownMenu'); + if (this.constants_.FIELD_DROPDOWN_COLOURED_DIV) { + var primaryColour = (this.sourceBlock_.isShadow()) ? + this.sourceBlock_.getParent().getColour() : + this.sourceBlock_.getColour(); + var borderColour = (this.sourceBlock_.isShadow()) ? + this.sourceBlock_.getParent().style.colourTertiary : + this.sourceBlock_.style.colourTertiary; + Blockly.DropDownDiv.setColour(primaryColour, borderColour); + } + Blockly.DropDownDiv.showPositionedByField( this, this.dropdownDispose_.bind(this)); @@ -224,17 +308,19 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() { /** @type {!Element} */ (this.selectedMenuItem_.getElement()), /** @type {!Element} */ (this.menu_.getElement())); } + + this.applyColour(); }; /** * Create the dropdown editor. - * @return {Blockly.Menu} The newly created dropdown menu. + * @return {!Blockly.Menu} The newly created dropdown menu. * @private */ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() { var menu = new Blockly.Menu(); menu.setRightToLeft(this.sourceBlock_.RTL); - menu.setRole('listbox'); + menu.setRole(Blockly.utils.aria.Role.LISTBOX); var options = this.getOptions(false); this.selectedMenuItem_ = null; @@ -249,7 +335,7 @@ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() { content = image; } var menuItem = new Blockly.MenuItem(content); - menuItem.setRole('option'); + menuItem.setRole(Blockly.utils.aria.Role.OPTION); menuItem.setRightToLeft(this.sourceBlock_.RTL); menuItem.setValue(value); menuItem.setCheckable(true); @@ -269,12 +355,16 @@ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() { }; /** - * Dispose of events belonging to the dropdown editor. + * Disposes of events and dom-references belonging to the dropdown editor. * @private */ Blockly.FieldDropdown.prototype.dropdownDispose_ = function() { - this.menu_.dispose(); + if (this.menu_) { + this.menu_.dispose(); + } this.menu_ = null; + this.selectedMenuItem_ = null; + this.applyColour(); }; /** @@ -284,15 +374,16 @@ Blockly.FieldDropdown.prototype.dropdownDispose_ = function() { */ Blockly.FieldDropdown.prototype.handleMenuActionEvent_ = function(menuItem) { Blockly.DropDownDiv.hideIfOwner(this, true); - this.onItemSelected(this.menu_, menuItem); + this.onItemSelected_(/** @type {!Blockly.Menu} */ (this.menu_), menuItem); }; /** * Handle the selection of an item in the dropdown menu. * @param {!Blockly.Menu} menu The Menu component clicked. * @param {!Blockly.MenuItem} menuItem The MenuItem selected within menu. + * @protected */ -Blockly.FieldDropdown.prototype.onItemSelected = function(menu, menuItem) { +Blockly.FieldDropdown.prototype.onItemSelected_ = function(menu, menuItem) { this.setValue(menuItem.getValue()); }; @@ -408,7 +499,7 @@ Blockly.FieldDropdown.prototype.getOptions = function(opt_useCache) { Blockly.FieldDropdown.prototype.doClassValidation_ = function(opt_newValue) { var isValueValid = false; var options = this.getOptions(true); - for (var i = 0, option; option = options[i]; i++) { + for (var i = 0, option; (option = options[i]); i++) { // Options are tuples of human-readable text and language-neutral values. if (option[1] == opt_newValue) { isValueValid = true; @@ -435,9 +526,9 @@ Blockly.FieldDropdown.prototype.doClassValidation_ = function(opt_newValue) { Blockly.FieldDropdown.prototype.doValueUpdate_ = function(newValue) { Blockly.FieldDropdown.superClass_.doValueUpdate_.call(this, newValue); var options = this.getOptions(true); - for (var i = 0, option; option = options[i]; i++) { + for (var i = 0, option; (option = options[i]); i++) { if (option[1] == this.value_) { - this.selectedIndex_ = i; + this.selectedOption_ = option; } } }; @@ -446,13 +537,23 @@ Blockly.FieldDropdown.prototype.doValueUpdate_ = function(newValue) { * Updates the dropdown arrow to match the colour/style of the block. * @package */ -Blockly.FieldDropdown.prototype.updateColour = function() { +Blockly.FieldDropdown.prototype.applyColour = function() { + if (this.borderRect_) { + this.borderRect_.setAttribute('stroke', + this.sourceBlock_.style.colourTertiary); + if (this.menu_) { + this.borderRect_.setAttribute('fill', + this.sourceBlock_.style.colourTertiary); + } else { + this.borderRect_.setAttribute('fill', 'transparent'); + } + } // Update arrow's colour. if (this.sourceBlock_ && this.arrow_) { if (this.sourceBlock_.isShadow()) { - this.arrow_.style.fill = this.sourceBlock_.getColourShadow(); + this.arrow_.style.fill = this.sourceBlock_.style.colourSecondary; } else { - this.arrow_.style.fill = this.sourceBlock_.getColour(); + this.arrow_.style.fill = this.sourceBlock_.style.colourPrimary; } } }; @@ -467,16 +568,17 @@ Blockly.FieldDropdown.prototype.render_ = function() { this.imageElement_.style.display = 'none'; // Show correct element. - var options = this.getOptions(true); - var selectedOption = this.selectedIndex_ >= 0 && - options[this.selectedIndex_][0]; - if (selectedOption && typeof selectedOption == 'object') { - this.renderSelectedImage_(selectedOption); + var option = this.selectedOption_ && this.selectedOption_[0]; + if (option && typeof option == 'object') { + this.renderSelectedImage_( + /** @type {!Blockly.FieldDropdown.ImageProperties} */ (option)); } else { this.renderSelectedText_(); } - this.borderRect_.setAttribute('height', this.size_.height); - this.borderRect_.setAttribute('width', this.size_.width); + if (this.borderRect_) { + this.borderRect_.setAttribute('height', this.size_.height); + this.borderRect_.setAttribute('width', this.size_.width); + } }; /** @@ -492,27 +594,41 @@ Blockly.FieldDropdown.prototype.renderSelectedImage_ = function(imageJson) { this.imageElement_.setAttribute('height', imageJson.height); this.imageElement_.setAttribute('width', imageJson.width); - var arrowWidth = Blockly.utils.dom.getTextWidth(this.arrow_); - var imageHeight = Number(imageJson.height); var imageWidth = Number(imageJson.width); // Height and width include the border rect. - this.size_.height = imageHeight + Blockly.FieldDropdown.IMAGE_Y_PADDING; - this.size_.width = imageWidth + arrowWidth + Blockly.Field.X_PADDING; + var hasBorder = !!this.borderRect_; + this.size_.height = Math.max( + hasBorder ? this.constants_.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, + imageHeight + Blockly.FieldDropdown.IMAGE_Y_PADDING); + var halfHeight = this.size_.height / 2; + var xPadding = hasBorder ? this.constants_.FIELD_BORDER_RECT_X_PADDING : 0; + var arrowWidth = 0; + if (this.svgArrow_) { + arrowWidth = this.positionSVGArrow_(imageWidth + xPadding, halfHeight - + this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2); + } else { + arrowWidth = Blockly.utils.dom.getFastTextWidth( + /** @type {!SVGTSpanElement} */ (this.arrow_), + this.constants_.FIELD_TEXT_FONTSIZE, + this.constants_.FIELD_TEXT_FONTWEIGHT, + this.constants_.FIELD_TEXT_FONTFAMILY); + } + this.size_.width = imageWidth + arrowWidth + xPadding * 2; if (this.sourceBlock_.RTL) { - var imageX = Blockly.Field.DEFAULT_TEXT_OFFSET + arrowWidth; - var arrowX = Blockly.Field.DEFAULT_TEXT_OFFSET - 1; + var imageX = xPadding + arrowWidth; + var arrowX = xPadding - 1; this.imageElement_.setAttribute('x', imageX); this.textElement_.setAttribute('x', arrowX); } else { - var arrowX = - imageWidth + arrowWidth + Blockly.Field.DEFAULT_TEXT_OFFSET + 1; + var arrowX = imageWidth + arrowWidth + xPadding + 1; this.textElement_.setAttribute('text-anchor', 'end'); this.textElement_.setAttribute('x', arrowX); - this.imageElement_.setAttribute('x', Blockly.Field.DEFAULT_TEXT_OFFSET); + this.imageElement_.setAttribute('x', xPadding); } + this.imageElement_.setAttribute('y', halfHeight - imageHeight / 2); }; /** @@ -522,32 +638,77 @@ Blockly.FieldDropdown.prototype.renderSelectedImage_ = function(imageJson) { Blockly.FieldDropdown.prototype.renderSelectedText_ = function() { // Retrieves the selected option to display through getText_. this.textContent_.nodeValue = this.getDisplayText_(); + Blockly.utils.dom.addClass(/** @type {!Element} */ (this.textElement_), + 'blocklyDropdownText'); this.textElement_.setAttribute('text-anchor', 'start'); - this.textElement_.setAttribute('x', Blockly.Field.DEFAULT_TEXT_OFFSET); + // Height and width include the border rect. - this.size_.height = Blockly.Field.BORDER_RECT_DEFAULT_HEIGHT; - this.size_.width = Blockly.utils.dom.getTextWidth(this.textElement_) + - Blockly.Field.X_PADDING; + var hasBorder = !!this.borderRect_; + this.size_.height = Math.max( + hasBorder ? this.constants_.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0, + this.constants_.FIELD_TEXT_HEIGHT); + var halfHeight = this.size_.height / 2; + var textWidth = Blockly.utils.dom.getFastTextWidth(this.textElement_, + this.constants_.FIELD_TEXT_FONTSIZE, + this.constants_.FIELD_TEXT_FONTWEIGHT, + this.constants_.FIELD_TEXT_FONTFAMILY); + var xPadding = hasBorder ? this.constants_.FIELD_BORDER_RECT_X_PADDING : 0; + var arrowWidth = 0; + if (this.svgArrow_) { + arrowWidth = this.positionSVGArrow_(textWidth + xPadding, halfHeight - + this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2); + } + this.size_.width = textWidth + arrowWidth + xPadding * 2; + + this.textElement_.setAttribute('x', this.sourceBlock_.RTL ? + this.size_.width - textWidth - xPadding : xPadding); + this.textElement_.setAttribute('y', halfHeight); + if (!this.constants_.FIELD_TEXT_BASELINE_CENTER) { + this.textElement_.setAttribute('dy', + this.constants_.FIELD_TEXT_BASELINE_Y - + this.constants_.FIELD_TEXT_HEIGHT / 2 + + this.constants_.FIELD_TEXT_Y_OFFSET); + } }; /** - * Use the `getText_` developer hook to override the field's text representation. - * Get the selected option text. If the selected option is an image - * we return the image alt text. + * Position a drop-down arrow at the appropriate location at render-time. + * @param {number} x X position the arrow is being rendered at, in px. + * @param {number} y Y position the arrow is being rendered at, in px. + * @return {number} Amount of space the arrow is taking up, in px. + * @private + */ +Blockly.FieldDropdown.prototype.positionSVGArrow_ = function(x, y) { + if (!this.svgArrow_) { + return 0; + } + var hasBorder = !!this.borderRect_; + var xPadding = hasBorder ? this.constants_.FIELD_BORDER_RECT_X_PADDING : 0; + var textPadding = this.constants_.FIELD_DROPDOWN_SVG_ARROW_PADDING; + var svgArrowSize = this.constants_.FIELD_DROPDOWN_SVG_ARROW_SIZE; + var arrowX = this.sourceBlock_.RTL ? xPadding : x + textPadding; + this.svgArrow_.setAttribute('transform', + 'translate(' + arrowX + ',' + y + ')'); + return svgArrowSize + textPadding; +}; + +/** + * Use the `getText_` developer hook to override the field's text + * representation. Get the selected option text. If the selected option is an + * image we return the image alt text. * @return {?string} Selected option text. * @protected * @override */ Blockly.FieldDropdown.prototype.getText_ = function() { - if (this.selectedIndex_ < 0) { + if (!this.selectedOption_) { return null; } - var options = this.getOptions(true); - var selectedOption = options[this.selectedIndex_][0]; - if (typeof selectedOption == 'object') { - return selectedOption['alt']; + var option = this.selectedOption_[0]; + if (typeof option == 'object') { + return option['alt']; } - return selectedOption; + return option; }; /** @@ -611,4 +772,5 @@ Blockly.FieldDropdown.prototype.onBlocklyAction = function(action) { return Blockly.FieldDropdown.superClass_.onBlocklyAction.call(this, action); }; + Blockly.fieldRegistry.register('field_dropdown', Blockly.FieldDropdown); diff --git a/core/field_image.js b/core/field_image.js index cd7d0df10..f60c40a92 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -115,6 +115,13 @@ Blockly.FieldImage = function(src, width, height, if (typeof opt_onClick == 'function') { this.clickHandler_ = opt_onClick; } + + /** + * The rendered field's image element. + * @type {SVGImageElement} + * @private + */ + this.imageElement_ = null; }; Blockly.utils.object.inherits(Blockly.FieldImage, Blockly.Field); @@ -173,14 +180,15 @@ Blockly.FieldImage.prototype.configure_ = function(config) { * @package */ 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_ = /** @type {!SVGImageElement} */ + (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', /** @type {string} */ (this.value_)); }; @@ -208,7 +216,7 @@ Blockly.FieldImage.prototype.doValueUpdate_ = function(newValue) { this.value_ = newValue; if (this.imageElement_) { this.imageElement_.setAttributeNS(Blockly.utils.dom.XLINK_NS, - 'xlink:href', this.value_ || ''); + 'xlink:href', String(this.value_)); } }; @@ -239,6 +247,7 @@ Blockly.FieldImage.prototype.setAlt = function(alt) { /** * If field click is called, and click handler defined, * call the handler. + * @protected */ Blockly.FieldImage.prototype.showEditor_ = function() { if (this.clickHandler_) { diff --git a/core/field_label.js b/core/field_label.js index a7bf7d156..510a457d7 100644 --- a/core/field_label.js +++ b/core/field_label.js @@ -60,14 +60,6 @@ Blockly.FieldLabel = function(opt_value, opt_class, opt_config) { if (!opt_config) { // If the config was not passed use old configuration. this.class_ = opt_class || null; } - - /** - * The size of the area rendered by the field. - * @type {Blockly.utils.Size} - * @protected - * @override - */ - this.size_ = new Blockly.utils.Size(0, Blockly.Field.TEXT_DEFAULT_HEIGHT); }; Blockly.utils.object.inherits(Blockly.FieldLabel, Blockly.Field); @@ -105,10 +97,9 @@ Blockly.FieldLabel.prototype.configure_ = function(config) { */ Blockly.FieldLabel.prototype.initView = function() { this.createTextElement_(); - // The y attribute of an SVG text element is the baseline. - this.textElement_.setAttribute('y', this.size_.height); if (this.class_) { - Blockly.utils.dom.addClass(this.textElement_, this.class_); + Blockly.utils.dom.addClass( + /** @type {!SVGTextElement} */ (this.textElement_), this.class_); } }; diff --git a/core/field_multilineinput.js b/core/field_multilineinput.js index 2b1f21c73..df4293e5b 100644 --- a/core/field_multilineinput.js +++ b/core/field_multilineinput.js @@ -29,6 +29,7 @@ goog.require('Blockly.Css'); goog.require('Blockly.DropDownDiv'); goog.require('Blockly.FieldTextInput'); goog.require('Blockly.utils'); +goog.require('Blockly.utils.aria'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.KeyCodes'); @@ -58,6 +59,13 @@ Blockly.FieldMultilineInput = function(opt_value, opt_validator, opt_config) { } Blockly.FieldMultilineInput.superClass_.constructor.call(this, opt_value, opt_validator, opt_config); + + /** + * The SVG group element that will contain a text element for each text row + * when initialized. + * @type {SVGGElement} + */ + this.textGroup_ = null; }; Blockly.utils.object.inherits(Blockly.FieldMultilineInput, Blockly.FieldTextInput); @@ -89,10 +97,11 @@ Blockly.FieldMultilineInput.fromJson = function(options) { */ Blockly.FieldMultilineInput.prototype.initView = function() { this.createBorderRect_(); - this.textGroup_ = Blockly.utils.dom.createSvgElement('g', - { - 'class': 'blocklyEditableText', - }, this.fieldGroup_); + this.textGroup_ = /** @type {!SVGGElement} **/ + (Blockly.utils.dom.createSvgElement('g', + { + 'class': 'blocklyEditableText', + }, this.fieldGroup_)); }; /** @@ -137,19 +146,18 @@ Blockly.FieldMultilineInput.prototype.getDisplayText_ = function() { Blockly.FieldMultilineInput.prototype.render_ = function() { // Remove all text group children. var currentChild; - while (currentChild = this.textGroup_.firstChild) { + while ((currentChild = this.textGroup_.firstChild)) { this.textGroup_.removeChild(currentChild); } // Add in text elements into the group. var lines = this.getDisplayText_().split('\n'); - var yOffset = Blockly.Field.Y_PADDING / 2; var y = 0; for (var i = 0; i < lines.length; i++) { var span = Blockly.utils.dom.createSvgElement('text', { 'class': 'blocklyText blocklyMultilineText', - x: Blockly.Field.DEFAULT_TEXT_OFFSET, - y: y + yOffset, + x: this.constants_.FIELD_BORDER_RECT_X_PADDING, + y: y + this.constants_.FIELD_BORDER_RECT_Y_PADDING, dy: Blockly.FieldMultilineInput.LINE_HEIGHT / 2 }, this.textGroup_); span.appendChild(document.createTextNode(lines[i])); @@ -167,12 +175,15 @@ Blockly.FieldMultilineInput.prototype.render_ = function() { } else { this.resizeEditor_(); } + var htmlInput = /** @type {!HTMLElement} */(this.htmlInput_); if (!this.isTextValid_) { - Blockly.utils.dom.addClass(this.htmlInput_, 'blocklyInvalidInput'); - Blockly.utils.aria.setState(this.htmlInput_, 'invalid', true); + Blockly.utils.dom.addClass(htmlInput, 'blocklyInvalidInput'); + Blockly.utils.aria.setState(htmlInput, + Blockly.utils.aria.State.INVALID, true); } else { - Blockly.utils.dom.removeClass(this.htmlInput_, 'blocklyInvalidInput'); - Blockly.utils.aria.setState(this.htmlInput_, 'invalid', false); + Blockly.utils.dom.removeClass(htmlInput, 'blocklyInvalidInput'); + Blockly.utils.aria.setState(htmlInput, + Blockly.utils.aria.State.INVALID, false); } } }; @@ -186,7 +197,7 @@ Blockly.FieldMultilineInput.prototype.updateSize_ = function() { var totalWidth = 0; var totalHeight = 0; for (var i = 0; i < nodes.length; i++) { - var tspan = nodes[i]; + var tspan = /** @type {!Element} */ (nodes[i]); var textWidth = Blockly.utils.dom.getTextWidth(tspan); if (textWidth > totalWidth) { totalWidth = textWidth; @@ -194,7 +205,7 @@ Blockly.FieldMultilineInput.prototype.updateSize_ = function() { totalHeight += Blockly.FieldMultilineInput.LINE_HEIGHT; } if (this.borderRect_) { - totalWidth += Blockly.Field.X_PADDING; + totalWidth += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2; this.borderRect_.setAttribute('width', totalWidth); this.borderRect_.setAttribute('height', totalHeight); } @@ -208,7 +219,7 @@ Blockly.FieldMultilineInput.prototype.updateSize_ = function() { */ Blockly.FieldMultilineInput.prototype.resizeEditor_ = function() { var div = Blockly.WidgetDiv.DIV; - var bBox = this.getScaledBBox_(); + var bBox = this.getScaledBBox(); div.style.width = bBox.right - bBox.left + 'px'; div.style.height = bBox.bottom - bBox.top + 'px'; @@ -233,12 +244,12 @@ Blockly.FieldMultilineInput.prototype.widgetCreate_ = function() { var htmlInput = /** @type {HTMLTextAreaElement} */ (document.createElement('textarea')); htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput'; htmlInput.setAttribute('spellcheck', this.spellcheck_); - var fontSize = (Blockly.FieldTextInput.FONTSIZE * scale) + 'pt'; + var fontSize = (this.constants_.FIELD_TEXT_FONTSIZE * scale) + 'pt'; div.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; var borderRadius = (Blockly.FieldTextInput.BORDERRADIUS * scale) + 'px'; htmlInput.style.borderRadius = borderRadius; - var padding = Blockly.Field.DEFAULT_TEXT_OFFSET * scale; + var padding = this.constants_.FIELD_BORDER_RECT_X_PADDING * scale; htmlInput.style.paddingLeft = padding + 'px'; htmlInput.style.width = 'calc(100% - ' + padding + 'px)'; htmlInput.style.lineHeight = diff --git a/core/field_number.js b/core/field_number.js index 396cac7f5..9cdd99439 100644 --- a/core/field_number.js +++ b/core/field_number.js @@ -25,6 +25,7 @@ goog.provide('Blockly.FieldNumber'); goog.require('Blockly.fieldRegistry'); goog.require('Blockly.FieldTextInput'); +goog.require('Blockly.utils.aria'); goog.require('Blockly.utils.object'); @@ -274,6 +275,8 @@ Blockly.FieldNumber.prototype.doClassValidation_ = function(opt_newValue) { newValue = newValue.replace(/O/ig, '0'); // Strip out thousands separators. newValue = newValue.replace(/,/g, ''); + // Ignore case of 'Infinity'. + newValue = newValue.replace(/infinity/i, 'Infinity'); // Clean up number. var n = Number(newValue || 0); diff --git a/core/field_textinput.js b/core/field_textinput.js index 46bcb7bc5..69b20bc54 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -64,6 +64,33 @@ Blockly.FieldTextInput = function(opt_value, opt_validator, opt_config) { } Blockly.FieldTextInput.superClass_.constructor.call(this, opt_value, opt_validator, opt_config); + + /** + * The HTML input element. + * @type {HTMLElement} + */ + this.htmlInput_ = null; + + /** + * Key down event data. + * @type {?Blockly.EventData} + * @private + */ + this.onKeyDownWrapper_ = null; + + /** + * Key input event data. + * @type {?Blockly.EventData} + * @private + */ + this.onKeyInputWrapper_ = null; + + /** + * Whether the field should consider the whole parent block to be its click + * target. + * @type {?boolean} + */ + this.fullBlockClickTarget_ = false; }; Blockly.utils.object.inherits(Blockly.FieldTextInput, Blockly.Field); @@ -87,11 +114,6 @@ Blockly.FieldTextInput.fromJson = function(options) { */ Blockly.FieldTextInput.prototype.SERIALIZABLE = true; -/** - * Point size of text. Should match blocklyText's font-size in CSS. - */ -Blockly.FieldTextInput.FONTSIZE = 11; - /** * Pixel size of input border radius. * Should match blocklyText's border-radius in CSS. @@ -113,6 +135,41 @@ Blockly.FieldTextInput.prototype.configure_ = function(config) { } }; +/** + * @override + */ +Blockly.FieldTextInput.prototype.initView = function() { + if (this.constants_.FULL_BLOCK_FIELDS) { + // Step one: figure out if this is the only field on this block. + // Rendering is quite different in that case. + var nFields = 0; + var nConnections = 0; + + // Count the number of fields, excluding text fields + for (var i = 0, input; (input = this.sourceBlock_.inputList[i]); i++) { + for (var j = 0; (input.fieldRow[j]); j++) { + nFields ++; + } + if (input.connection) { + nConnections++; + } + } + // The special case is when this is the only non-label field on the block + // and it has an output but no inputs. + this.fullBlockClickTarget_ = + nFields <= 1 && this.sourceBlock_.outputConnection && !nConnections; + } else { + this.fullBlockClickTarget_ = false; + } + + if (this.fullBlockClickTarget_) { + this.clickTarget_ = this.sourceBlock_.getSvgRoot(); + } else { + this.createBorderRect_(); + } + this.createTextElement_(); +}; + /** * Ensure that the input value casts to a valid string. * @param {*=} opt_newValue The input value. @@ -165,6 +222,22 @@ Blockly.FieldTextInput.prototype.doValueUpdate_ = function(newValue) { } }; +/** + * Updates text field to match the colour/style of the block. + * @package + */ +Blockly.FieldTextInput.prototype.applyColour = function() { + if (this.sourceBlock_ && this.constants_.FULL_BLOCK_FIELDS) { + if (this.borderRect_) { + this.borderRect_.setAttribute('stroke', + this.sourceBlock_.style.colourTertiary); + } else { + this.sourceBlock_.pathObject.svgPath.setAttribute('fill', + this.constants_.FIELD_BORDER_RECT_COLOUR); + } + } +}; + /** * Updates the colour of the htmlInput given the current validity of the * field's value. @@ -175,20 +248,16 @@ Blockly.FieldTextInput.prototype.render_ = function() { // This logic is done in render_ rather than doValueInvalid_ or // doValueUpdate_ so that the code is more centralized. if (this.isBeingEdited_) { - if (this.sourceBlock_.RTL) { - // in RTL, we need to let the browser reflow before resizing - // in order to get the correct bounding box of the borderRect - // avoiding issue #2777. - setTimeout(this.resizeEditor_.bind(this), 0); - } else { - this.resizeEditor_(); - } + this.resizeEditor_(); + var htmlInput = /** @type {!HTMLElement} */(this.htmlInput_); if (!this.isTextValid_) { - Blockly.utils.dom.addClass(this.htmlInput_, 'blocklyInvalidInput'); - Blockly.utils.aria.setState(this.htmlInput_, 'invalid', true); + Blockly.utils.dom.addClass(htmlInput, 'blocklyInvalidInput'); + Blockly.utils.aria.setState(htmlInput, + Blockly.utils.aria.State.INVALID, true); } else { - Blockly.utils.dom.removeClass(this.htmlInput_, 'blocklyInvalidInput'); - Blockly.utils.aria.setState(this.htmlInput_, 'invalid', false); + Blockly.utils.dom.removeClass(htmlInput, 'blocklyInvalidInput'); + Blockly.utils.aria.setState(htmlInput, + Blockly.utils.aria.State.INVALID, false); } } }; @@ -209,11 +278,14 @@ Blockly.FieldTextInput.prototype.setSpellcheck = function(check) { /** * Show the inline free-text editor on top of the text. + * @param {Event=} _opt_e Optional mouse event that triggered the field to open, + * or undefined if triggered programatically. * @param {boolean=} opt_quietInput True if editor should be created without * focus. Defaults to false. * @protected */ -Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) { +Blockly.FieldTextInput.prototype.showEditor_ = function(_opt_e, + opt_quietInput) { this.workspace_ = this.sourceBlock_.workspace; var quietInput = opt_quietInput || false; if (!quietInput && (Blockly.utils.userAgent.MOBILE || @@ -251,7 +323,7 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { this.isBeingEdited_ = true; if (!quietInput) { - this.htmlInput_.focus(); + this.htmlInput_.focus({preventScroll:true}); this.htmlInput_.select(); } }; @@ -264,27 +336,45 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { Blockly.FieldTextInput.prototype.widgetCreate_ = function() { var div = Blockly.WidgetDiv.DIV; + Blockly.utils.dom.addClass(this.getClickTarget_(), 'editing'); + var htmlInput = /** @type {HTMLInputElement} */ (document.createElement('input')); htmlInput.className = 'blocklyHtmlInput'; htmlInput.setAttribute('spellcheck', this.spellcheck_); + var scale = this.workspace_.scale; var fontSize = - (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; + (this.constants_.FIELD_TEXT_FONTSIZE * scale) + 'pt'; div.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; var borderRadius = - (Blockly.FieldTextInput.BORDERRADIUS * this.workspace_.scale) + 'px'; + (Blockly.FieldTextInput.BORDERRADIUS * scale) + 'px'; + + if (this.fullBlockClickTarget_) { + var bBox = this.getScaledBBox(); + + // Override border radius. + borderRadius = (bBox.bottom - bBox.top) / 2 + 'px'; + // Pull stroke colour from the existing shadow block + var strokeColour = this.sourceBlock_.getParent() ? + this.sourceBlock_.getParent().style.colourTertiary : + this.sourceBlock_.style.colourTertiary; + htmlInput.style.border = (1 * scale) + 'px solid ' + strokeColour; + div.style.borderRadius = borderRadius; + div.style.transition = 'box-shadow 0.25s ease 0s'; + if (this.constants_.FIELD_TEXTINPUT_BOX_SHADOW) { + div.style.boxShadow = 'rgba(255, 255, 255, 0.3) 0px 0px 0px ' + + 4 * scale + 'px'; + } + } htmlInput.style.borderRadius = borderRadius; + div.appendChild(htmlInput); htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_); htmlInput.untypedDefaultValue_ = this.value_; htmlInput.oldValue_ = null; - if (Blockly.utils.userAgent.GECKO) { - // In FF, ensure the browser reflows before resizing to avoid issue #2777. - setTimeout(this.resizeEditor_.bind(this), 0); - } else { - this.resizeEditor_(); - } + + this.resizeEditor_(); this.bindInputEvents_(htmlInput); @@ -292,34 +382,32 @@ Blockly.FieldTextInput.prototype.widgetCreate_ = function() { }; /** - * Close the editor, save the results, and dispose any events bound to the - * text input's editor. + * Closes the editor, saves the results, and disposes of any events or + * dom-references belonging to the editor. * @private */ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { - // Finalize value. + // Non-disposal related things that we do when the editor closes. this.isBeingEdited_ = false; this.isTextValid_ = true; - - // Always re-render when the we close the editor as value - // set on the field's node may be inconsistent with the field's - // internal value. + // Make sure the field's node matches the field's internal value. this.forceRerender(); - - // Call onFinishEditing - // TODO: Get rid of this or make it less of a hack. + // TODO(#2496): Make this less of a hack. if (this.onFinishEditing_) { this.onFinishEditing_(this.value_); } - // Remove htmlInput events. + // Actual disposal. this.unbindInputEvents_(); - - // Delete style properties. var style = Blockly.WidgetDiv.DIV.style; style.width = 'auto'; style.height = 'auto'; style.fontSize = ''; + style.transition = ''; + style.boxShadow = ''; + this.htmlInput_ = null; + + Blockly.utils.dom.removeClass(this.getClickTarget_(), 'editing'); }; /** @@ -344,8 +432,14 @@ Blockly.FieldTextInput.prototype.bindInputEvents_ = function(htmlInput) { * @private */ Blockly.FieldTextInput.prototype.unbindInputEvents_ = function() { - Blockly.unbindEvent_(this.onKeyDownWrapper_); - Blockly.unbindEvent_(this.onKeyInputWrapper_); + if (this.onKeyDownWrapper_) { + Blockly.unbindEvent_(this.onKeyDownWrapper_); + this.onKeyDownWrapper_ = null; + } + if (this.onKeyInputWrapper_) { + Blockly.unbindEvent_(this.onKeyInputWrapper_); + this.onKeyInputWrapper_ = null; + } }; /** @@ -386,6 +480,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) { var value = this.getValueFromEditorText_(text); this.setValue(value); this.forceRerender(); + this.resizeEditor_(); Blockly.Events.setGroup(false); } }; @@ -415,7 +510,7 @@ Blockly.FieldTextInput.prototype.setEditorValue_ = function(newValue) { */ Blockly.FieldTextInput.prototype.resizeEditor_ = function() { var div = Blockly.WidgetDiv.DIV; - var bBox = this.getScaledBBox_(); + var bBox = this.getScaledBBox(); div.style.width = bBox.right - bBox.left + 'px'; div.style.height = bBox.bottom - bBox.top + 'px'; @@ -424,17 +519,6 @@ Blockly.FieldTextInput.prototype.resizeEditor_ = function() { var x = this.sourceBlock_.RTL ? bBox.right - div.offsetWidth : bBox.left; var xy = new Blockly.utils.Coordinate(x, bBox.top); - // Shift by a few pixels to line up exactly. - xy.y += 1; - if (Blockly.utils.userAgent.GECKO && Blockly.WidgetDiv.DIV.style.top) { - // Firefox mis-reports the location of the border by a pixel - // once the WidgetDiv is moved into position. - xy.x -= 1; - xy.y -= 1; - } - if (Blockly.utils.userAgent.WEBKIT) { - xy.y -= 3; - } div.style.left = xy.x + 'px'; div.style.top = xy.y + 'px'; }; @@ -507,7 +591,7 @@ Blockly.FieldTextInput.prototype.getText_ = function() { * than the field's value. This should be coupled with an override of * `getValueFromEditorText_`. * @param {*} value The value stored in this field. - * @returns {string} The text to show on the html input. + * @return {string} The text to show on the html input. * @protected */ Blockly.FieldTextInput.prototype.getEditorText_ = function(value) { @@ -521,7 +605,7 @@ Blockly.FieldTextInput.prototype.getEditorText_ = function(value) { * than the field's value. This should be coupled with an override of * `getEditorText_`. * @param {string} text Text received from the html input. - * @returns {*} The value to store. + * @return {*} The value to store. * @protected */ Blockly.FieldTextInput.prototype.getValueFromEditorText_ = function(text) { diff --git a/core/field_variable.js b/core/field_variable.js index 159a9914e..8d486d197 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -63,7 +63,8 @@ Blockly.FieldVariable = function(varName, opt_validator, opt_variableTypes, /** * An array of options for a dropdown list, * or a function which generates these options. - * @type {!function(this:Blockly.FieldVariable): !Array.} + * @type {(!Array.| + * !function(this:Blockly.FieldDropdown): !Array.)} * @protected */ this.menuGenerator_ = Blockly.FieldVariable.dropdownCreate; @@ -82,7 +83,7 @@ Blockly.FieldVariable = function(varName, opt_validator, opt_variableTypes, * @protected * @override */ - this.size_ = new Blockly.utils.Size(0, Blockly.BlockSvg.MIN_BLOCK_Y); + this.size_ = new Blockly.utils.Size(0, 0); opt_config && this.configure_(opt_config); opt_validator && this.setValidator(opt_validator); @@ -146,11 +147,17 @@ Blockly.FieldVariable.prototype.initModel = function() { this.sourceBlock_.workspace, null, this.defaultVariableName, this.defaultType_); - // Don't fire a change event for this setValue. It would have null as the - // old value, which is not valid. - Blockly.Events.disable(); - this.setValue(variable.getId()); - Blockly.Events.enable(); + // Don't call setValue because we don't want to cause a rerender. + this.doValueUpdate_(variable.getId()); +}; + +/** + * @override + */ +Blockly.FieldVariable.prototype.shouldAddBorderRect_ = function() { + return Blockly.FieldVariable.superClass_.shouldAddBorderRect_.call(this) && + (!this.constants_.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW || + this.sourceBlock_.type != 'variables_get'); }; /** @@ -440,8 +447,9 @@ Blockly.FieldVariable.dropdownCreate = function() { * In the rename case, prompt the user for a new name. * @param {!Blockly.Menu} menu The Menu component clicked. * @param {!Blockly.MenuItem} menuItem The MenuItem selected within menu. + * @protected */ -Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { +Blockly.FieldVariable.prototype.onItemSelected_ = function(menu, menuItem) { var id = menuItem.getValue(); // Handle special cases. if (this.sourceBlock_ && this.sourceBlock_.workspace) { diff --git a/core/flyout_base.js b/core/flyout_base.js index 24326655d..471c4d558 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -30,7 +30,7 @@ goog.require('Blockly.Events.BlockCreate'); goog.require('Blockly.Events.VarCreate'); goog.require('Blockly.FlyoutCursor'); goog.require('Blockly.Gesture'); -goog.require('Blockly.MarkerCursor'); +goog.require('Blockly.Marker'); goog.require('Blockly.Scrollbar'); goog.require('Blockly.Tooltip'); goog.require('Blockly.Touch'); @@ -43,7 +43,8 @@ goog.require('Blockly.Xml'); /** * Class for a flyout. - * @param {!Object} workspaceOptions Dictionary of options for the workspace. + * @param {!Blockly.Options} workspaceOptions Dictionary of options for the + * workspace. * @constructor */ Blockly.Flyout = function(workspaceOptions) { @@ -56,8 +57,6 @@ Blockly.Flyout = function(workspaceOptions) { */ this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); this.workspace_.isFlyout = true; - this.workspace_.setCursor(new Blockly.FlyoutCursor()); - this.workspace_.setMarker(new Blockly.MarkerCursor()); /** * Is RTL vs LTR. @@ -111,6 +110,7 @@ Blockly.Flyout = function(workspaceOptions) { /** * Width of output tab. * @type {number} + * @protected * @const */ this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH; @@ -227,15 +227,18 @@ Blockly.Flyout.prototype.createDom = function(tagName) { this.svgBackground_ = Blockly.utils.dom.createSvgElement('path', {'class': 'blocklyFlyoutBackground'}, this.svgGroup_); this.svgGroup_.appendChild(this.workspace_.createDom()); - this.workspace_.getThemeManager().subscribe(this.svgBackground_, 'flyout', 'fill'); - this.workspace_.getThemeManager().subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); + this.workspace_.getThemeManager().subscribe( + this.svgBackground_, 'flyoutBackgroundColour', 'fill'); + this.workspace_.getThemeManager().subscribe( + this.svgBackground_, 'flyoutOpacity', 'fill-opacity'); + this.workspace_.getMarkerManager().setCursor(new Blockly.FlyoutCursor()); return this.svgGroup_; }; /** * Initializes the flyout. - * @param {!Blockly.Workspace} targetWorkspace The workspace in which to create - * new blocks. + * @param {!Blockly.WorkspaceSvg} targetWorkspace The workspace in which to + * create new blocks. */ Blockly.Flyout.prototype.init = function(targetWorkspace) { this.targetWorkspace_ = targetWorkspace; @@ -264,7 +267,7 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) { this.targetWorkspace_.getGesture.bind(this.targetWorkspace_); // Get variables from the main workspace rather than the target workspace. - this.workspace_.variableMap_ = this.targetWorkspace_.getVariableMap(); + this.workspace_.setVariableMap(this.targetWorkspace_.getVariableMap()); this.workspace_.createPotentialVariableMap(); }; @@ -272,6 +275,7 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) { /** * Dispose of this flyout. * Unlink from all DOM elements to prevent memory leaks. + * @suppress {checkTypes} */ Blockly.Flyout.prototype.dispose = function() { this.hide(); @@ -417,7 +421,7 @@ Blockly.Flyout.prototype.hide = function() { } this.setVisible(false); // Delete all the event listeners. - for (var i = 0, listen; listen = this.listeners_[i]; i++) { + for (var i = 0, listen; (listen = this.listeners_[i]); i++) { Blockly.unbindEvent_(listen); } this.listeners_.length = 0; @@ -431,7 +435,7 @@ Blockly.Flyout.prototype.hide = function() { /** * Show and populate the flyout. - * @param {!Array|string} xmlList List of blocks to show. + * @param {!Array|!NodeList|string} xmlList List of blocks to show. * Variables and procedures have a custom set of blocks. */ Blockly.Flyout.prototype.show = function(xmlList) { @@ -461,7 +465,7 @@ Blockly.Flyout.prototype.show = function(xmlList) { 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++) { + for (var i = 0, xml; (xml = xmlList[i]); i++) { if (!xml.tagName) { continue; } @@ -512,7 +516,7 @@ Blockly.Flyout.prototype.show = function(xmlList) { // When the mouse is over the background, deselect all blocks. var deselectAll = function() { var topBlocks = this.workspace_.getTopBlocks(false); - for (var i = 0, block; block = topBlocks[i]; i++) { + for (var i = 0, block; (block = topBlocks[i]); i++) { block.removeSelect(); } }; @@ -544,7 +548,7 @@ Blockly.Flyout.prototype.show = function(xmlList) { Blockly.Flyout.prototype.clearOldBlocks_ = function() { // Delete any blocks from a previous showing. var oldBlocks = this.workspace_.getTopBlocks(false); - for (var i = 0, block; block = oldBlocks[i]; i++) { + for (var i = 0, block; (block = oldBlocks[i]); i++) { if (block.workspace == this.workspace_) { block.dispose(false, false); } @@ -558,7 +562,7 @@ Blockly.Flyout.prototype.clearOldBlocks_ = function() { } this.mats_.length = 0; // Delete any buttons from a previous showing. - for (var i = 0, button; button = this.buttons_[i]; i++) { + for (var i = 0, button; (button = this.buttons_[i]); i++) { button.dispose(); } this.buttons_.length = 0; @@ -580,13 +584,13 @@ Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) { this.blockMouseDown_(block))); this.listeners_.push(Blockly.bindEventWithChecks_(rect, 'mousedown', null, this.blockMouseDown_(block))); - this.listeners_.push(Blockly.bindEvent_(root, 'mouseover', block, + this.listeners_.push(Blockly.bindEvent_(root, 'mouseenter', block, block.addSelect)); - this.listeners_.push(Blockly.bindEvent_(root, 'mouseout', block, + this.listeners_.push(Blockly.bindEvent_(root, 'mouseleave', block, block.removeSelect)); - this.listeners_.push(Blockly.bindEvent_(rect, 'mouseover', block, + this.listeners_.push(Blockly.bindEvent_(rect, 'mouseenter', block, block.addSelect)); - this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block, + this.listeners_.push(Blockly.bindEvent_(rect, 'mouseleave', block, block.removeSelect)); }; @@ -751,7 +755,7 @@ Blockly.Flyout.prototype.moveRectToBlock_ = function(rect, block) { */ Blockly.Flyout.prototype.filterForCapacity_ = function() { var blocks = this.workspace_.getTopBlocks(false); - for (var i = 0, block; block = blocks[i]; i++) { + for (var i = 0, block; (block = blocks[i]); i++) { if (this.permanentlyDisabled_.indexOf(block) == -1) { var enable = this.targetWorkspace_ .isCapacityAvailable(Blockly.utils.getBlockTypeCounts(block)); @@ -787,8 +791,8 @@ Blockly.Flyout.prototype.isScrollable = function() { /** * Copy a block from the flyout to the workspace and position it correctly. - * @param {!Blockly.Block} oldBlock The flyout block to copy. - * @return {!Blockly.Block} The new block in the main workspace. + * @param {!Blockly.BlockSvg} oldBlock The flyout block to copy. + * @return {!Blockly.BlockSvg} The new block in the main workspace. * @private */ Blockly.Flyout.prototype.placeNewBlock_ = function(oldBlock) { @@ -806,7 +810,8 @@ Blockly.Flyout.prototype.placeNewBlock_ = function(oldBlock) { // Using domToBlock instead of domToWorkspace means that the new block will be // placed at position (0, 0) in main workspace units. - var block = Blockly.Xml.domToBlock(xml, targetWorkspace); + var block = /** @type {!Blockly.BlockSvg} */ + (Blockly.Xml.domToBlock(xml, targetWorkspace)); var svgRootNew = block.getSvgRoot(); if (!svgRootNew) { throw Error('block is not rendered.'); @@ -841,3 +846,15 @@ Blockly.Flyout.prototype.placeNewBlock_ = function(oldBlock) { block.moveBy(finalOffset.x, finalOffset.y); return block; }; + +/** + * Handles the given action. + * This is only triggered when keyboard accessibility mode is enabled. + * @param {!Blockly.Action} action The action to be handled. + * @return {boolean} True if the flyout handled the action, false otherwise. + * @package + */ +Blockly.Flyout.prototype.onBlocklyAction = function(action) { + var cursor = this.workspace_.getCursor(); + return cursor.onBlocklyAction(action); +}; diff --git a/core/flyout_button.js b/core/flyout_button.js index 700933a06..f400f0fb6 100644 --- a/core/flyout_button.js +++ b/core/flyout_button.js @@ -87,6 +87,13 @@ Blockly.FlyoutButton = function(workspace, targetWorkspace, xml, isLabel) { * @private */ this.cssClass_ = xml.getAttribute('web-class') || null; + + /** + * Mouse up event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseUpWrapper_ = null; }; /** @@ -106,13 +113,6 @@ Blockly.FlyoutButton.prototype.width = 0; */ Blockly.FlyoutButton.prototype.height = 0; -/** - * Opaque data that can be passed to Blockly.unbindEvent_. - * @type {Array.} - * @private - */ -Blockly.FlyoutButton.prototype.onMouseUpWrapper_ = null; - /** * Create the button elements. * @return {!SVGElement} The button's SVG group. @@ -152,10 +152,16 @@ Blockly.FlyoutButton.prototype.createDom = function() { 'text-anchor': 'middle' }, this.svgGroup_); - svgText.textContent = Blockly.utils.replaceMessageReferences(this.text_); + var text = Blockly.utils.replaceMessageReferences(this.text_); + if (this.workspace_.RTL) { + // Force text to be RTL by adding an RLM. + text += '\u200F'; + } + svgText.textContent = text; if (this.isLabel_) { this.svgText_ = svgText; - this.workspace_.getThemeManager().subscribe(this.svgText_, 'flyoutText', 'fill'); + this.workspace_.getThemeManager().subscribe(this.svgText_, + 'flyoutForegroundColour', 'fill'); } this.width = Blockly.utils.dom.getTextWidth(svgText); diff --git a/core/flyout_horizontal.js b/core/flyout_horizontal.js index 199a8ea70..1bb701790 100644 --- a/core/flyout_horizontal.js +++ b/core/flyout_horizontal.js @@ -34,12 +34,14 @@ goog.require('Blockly.WidgetDiv'); /** * Class for a flyout. - * @param {!Object} workspaceOptions Dictionary of options for the workspace. + * @param {!Blockly.Options} workspaceOptions Dictionary of options for the + * workspace. * @extends {Blockly.Flyout} * @constructor */ Blockly.HorizontalFlyout = function(workspaceOptions) { - workspaceOptions.getMetrics = this.getMetrics_.bind(this); + workspaceOptions.getMetrics = /** @type {function():!Object} */ ( + this.getMetrics_.bind(this)); workspaceOptions.setMetrics = this.setMetrics_.bind(this); Blockly.HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions); @@ -203,7 +205,7 @@ Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, // Bottom. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, -this.CORNER_RADIUS, this.CORNER_RADIUS); - path.push('h', -1 * width); + path.push('h', -width); // Left. path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, -this.CORNER_RADIUS, -this.CORNER_RADIUS); @@ -352,7 +354,7 @@ Blockly.HorizontalFlyout.prototype.getClientRect = function() { var height = flyoutRect.height; return new Blockly.utils.Rect(-BIG_NUM, top + height, -BIG_NUM, BIG_NUM); } else { // Bottom. - return new Blockly.utils.Rect(top, -BIG_NUM, -BIG_NUM, BIG_NUM); + return new Blockly.utils.Rect(top, BIG_NUM, -BIG_NUM, BIG_NUM); } }; diff --git a/core/flyout_vertical.js b/core/flyout_vertical.js index ac380f303..0404ff205 100644 --- a/core/flyout_vertical.js +++ b/core/flyout_vertical.js @@ -35,12 +35,14 @@ goog.require('Blockly.WidgetDiv'); /** * Class for a flyout. - * @param {!Object} workspaceOptions Dictionary of options for the workspace. + * @param {!Blockly.Options} workspaceOptions Dictionary of options for the + * workspace. * @extends {Blockly.Flyout} * @constructor */ Blockly.VerticalFlyout = function(workspaceOptions) { - workspaceOptions.getMetrics = this.getMetrics_.bind(this); + workspaceOptions.getMetrics = /** @type {function():!Object} */ ( + this.getMetrics_.bind(this)); workspaceOptions.setMetrics = this.setMetrics_.bind(this); Blockly.VerticalFlyout.superClass_.constructor.call(this, workspaceOptions); @@ -257,11 +259,11 @@ Blockly.VerticalFlyout.prototype.layout_ = function(contents, gaps) { var cursorX = this.RTL ? margin : margin + this.tabWidth_; var cursorY = margin; - for (var i = 0, item; item = contents[i]; i++) { + for (var i = 0, item; (item = contents[i]); i++) { if (item.type == 'block') { var block = item.block; var allBlocks = block.getDescendants(false); - for (var j = 0, child; child = allBlocks[j]; j++) { + for (var j = 0, child; (child = allBlocks[j]); j++) { // Mark blocks as being inside a flyout. This is used to detect and // prevent the closure of the flyout if the user right-clicks on such a // block. @@ -331,30 +333,6 @@ Blockly.VerticalFlyout.prototype.getClientRect = function() { var width = flyoutRect.width; return new Blockly.utils.Rect(-BIG_NUM, BIG_NUM, -BIG_NUM, left + width); } else { // Right - // Firefox sometimes reports the wrong value for the client rect. - // See https://github.com/google/blockly/issues/1425 and - // https://bugzilla.mozilla.org/show_bug.cgi?id=1066435 - if (Blockly.utils.userAgent.GECKO && - this.targetWorkspace_ && this.targetWorkspace_.isMutator) { - // The position of the left side of the mutator workspace in pixels - // relative to the window origin. - var targetWsLeftPixels = - this.targetWorkspace_.svgGroup_.getBoundingClientRect().x; - // The client rect is in pixels relative to the window origin. When the - // browser gets the wrong value it reports that the flyout left is the - // same as the mutator workspace left. - // We know that in a mutator workspace with the flyout on the right, the - // visible area of the workspace should be more than ten pixels wide. If - // the browser reports that the flyout is within ten pixels of the left - // side of the workspace, ignore it and manually calculate the value. - if (Math.abs(targetWsLeftPixels - left) < 10) { - // If we're in a mutator, its scale is always 1, purely because of some - // oddities in our rendering optimizations. The actual scale is the - // same as the scale on the parent workspace. - var scale = this.targetWorkspace_.options.parentWorkspace.scale; - left += this.leftEdge_ * scale; - } - } return new Blockly.utils.Rect(-BIG_NUM, BIG_NUM, left, BIG_NUM); } }; @@ -383,7 +361,7 @@ Blockly.VerticalFlyout.prototype.reflowInternal_ = function() { flyoutWidth += Blockly.Scrollbar.scrollbarThickness; if (this.width_ != flyoutWidth) { - for (var i = 0, block; block = blocks[i]; i++) { + for (var i = 0, block; (block = blocks[i]); i++) { if (this.RTL) { // With the flyoutWidth known, right-align the blocks. var oldX = block.getRelativeToSurfaceXY().x; diff --git a/core/generator.js b/core/generator.js index fee9478b6..8a73d5f8c 100644 --- a/core/generator.js +++ b/core/generator.js @@ -101,7 +101,7 @@ Blockly.Generator.prototype.workspaceToCode = function(workspace) { var code = []; this.init(workspace); var blocks = workspace.getTopBlocks(true); - for (var i = 0, block; block = blocks[i]; i++) { + for (var i = 0, block; (block = blocks[i]); i++) { var line = this.blockToCode(block); if (Array.isArray(line)) { // Value blocks return tuples of code and operator order. @@ -351,7 +351,7 @@ Blockly.Generator.prototype.injectId = function(msg, block) { /** * Comma-separated list of reserved words. * @type {string} - * @private + * @protected */ Blockly.Generator.prototype.RESERVED_WORDS_ = ''; @@ -370,10 +370,32 @@ Blockly.Generator.prototype.addReservedWords = function(words) { * legitimately appear in a function definition (or comment), and it must * not confuse the regular expression parser. * @type {string} - * @private + * @protected */ Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; +/** + * A dictionary of definitions to be printed before the code. + * @type {Object} + * @protected + */ +Blockly.Generator.prototype.definitions_; + +/** + * A dictionary mapping desired function names in definitions_ to actual + * function names (to avoid collisions with user functions). + * @type {Object} + * @protected + */ +Blockly.Generator.prototype.functionNames_; + +/** + * A database of variable names. + * @type {Blockly.Names} + * @protected + */ +Blockly.Generator.prototype.variableDB_; + /** * Define a function to be included in the generated code. * The first time this is called with a given desiredName, the code is @@ -389,12 +411,12 @@ Blockly.Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; * @param {!Array.} code A list of statements. Use ' ' for indents. * @return {string} The actual name of the new function. This may differ * from desiredName if the former has already been taken by the user. - * @private + * @protected */ Blockly.Generator.prototype.provideFunction_ = function(desiredName, code) { if (!this.definitions_[desiredName]) { var functionName = this.variableDB_.getDistinctName(desiredName, - Blockly.Procedures.NAME_TYPE); + Blockly.PROCEDURE_CATEGORY_NAME); this.functionNames_[desiredName] = functionName; var codeText = code.join('\n').replace( this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); @@ -431,10 +453,12 @@ Blockly.Generator.prototype.init = function(_workspace) { * value blocks. * @param {!Blockly.Block} _block The current block. * @param {string} code The code created for this block. + * @param {boolean=} _opt_thisOnly True to generate code for only this + * statement. * @return {string} Code with comments and subsequent blocks added. - * @private + * @protected */ -Blockly.Generator.prototype.scrub_ = function(_block, code) { +Blockly.Generator.prototype.scrub_ = function(_block, code, _opt_thisOnly) { // Optionally override return code; }; diff --git a/core/gesture.js b/core/gesture.js index 7308f278a..1e069ec4e 100644 --- a/core/gesture.js +++ b/core/gesture.js @@ -24,6 +24,7 @@ goog.provide('Blockly.Gesture'); +goog.require('Blockly.ASTNode'); goog.require('Blockly.blockAnimations'); goog.require('Blockly.BlockDragger'); goog.require('Blockly.BubbleDragger'); @@ -31,6 +32,7 @@ goog.require('Blockly.constants'); goog.require('Blockly.Events'); goog.require('Blockly.Events.Ui'); goog.require('Blockly.FlyoutDragger'); +goog.require('Blockly.navigation'); goog.require('Blockly.Tooltip'); goog.require('Blockly.Touch'); goog.require('Blockly.utils'); @@ -38,7 +40,7 @@ goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.WorkspaceDragger'); -/* +/** * Note: In this file "start" refers to touchstart, mousedown, and pointerstart * events. "End" refers to touchend, mouseup, and pointerend events. */ @@ -57,16 +59,17 @@ Blockly.Gesture = function(e, creatorWorkspace) { * The position of the mouse when the gesture started. Units are CSS pixels, * with (0, 0) at the top left of the browser window (mouseEvent clientX/Y). * @type {Blockly.utils.Coordinate} + * @private */ this.mouseDownXY_ = null; /** * How far the mouse has moved during this drag, in pixel units. * (0, 0) is at this.mouseDownXY_. - * @type {Blockly.utils.Coordinate} + * @type {!Blockly.utils.Coordinate} * @private */ - this.currentDragDeltaXY_ = null; + this.currentDragDeltaXY_ = new Blockly.utils.Coordinate(0, 0); /** * The bubble that the gesture started on, or null if it did not start on a @@ -116,7 +119,7 @@ Blockly.Gesture = function(e, creatorWorkspace) { * to the gesture, which will need to be cleared at deletion. * This may be different from the start workspace. For instance, a flyout is * a workspace, but its parent workspace manages gestures for it. - * @type {Blockly.WorkspaceSvg} + * @type {!Blockly.WorkspaceSvg} * @private */ this.creatorWorkspace_ = creatorWorkspace; @@ -161,7 +164,7 @@ Blockly.Gesture = function(e, creatorWorkspace) { /** * A handle to use to unbind a mouse move listener at the end of a drag. * Opaque data returned from Blockly.bindEventWithChecks_. - * @type {Array.} + * @type {?Blockly.EventData} * @protected */ this.onMoveWrapper_ = null; @@ -169,7 +172,7 @@ Blockly.Gesture = function(e, creatorWorkspace) { /** * A handle to use to unbind a mouse up listener at the end of a drag. * Opaque data returned from Blockly.bindEventWithChecks_. - * @type {Array.} + * @type {?Blockly.EventData} * @protected */ this.onUpWrapper_ = null; @@ -250,24 +253,14 @@ Blockly.Gesture.prototype.dispose = function() { Blockly.unbindEvent_(this.onUpWrapper_); } - - this.startField_ = null; - this.startBlock_ = null; - this.targetBlock_ = null; - this.startWorkspace_ = null; - this.flyout_ = null; - if (this.blockDragger_) { this.blockDragger_.dispose(); - this.blockDragger_ = null; } if (this.workspaceDragger_) { this.workspaceDragger_.dispose(); - this.workspaceDragger_ = null; } if (this.bubbleDragger_) { this.bubbleDragger_.dispose(); - this.bubbleDragger_ = null; } }; @@ -297,7 +290,7 @@ Blockly.Gesture.prototype.updateFromEvent_ = function(e) { */ Blockly.Gesture.prototype.updateDragDelta_ = function(currentXY) { this.currentDragDeltaXY_ = Blockly.utils.Coordinate.difference(currentXY, - this.mouseDownXY_); + /** @type {!Blockly.utils.Coordinate} */ (this.mouseDownXY_)); if (!this.hasExceededDragRadius_) { var currentDragDelta = Blockly.utils.Coordinate.magnitude( @@ -324,6 +317,9 @@ Blockly.Gesture.prototype.updateDragDelta_ = function(currentXY) { * @private */ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() { + if (!this.targetBlock_) { + return false; + } if (!this.flyout_.isBlockCreatable_(this.targetBlock_)) { return false; } @@ -410,7 +406,8 @@ Blockly.Gesture.prototype.updateIsDraggingWorkspace_ = function() { if (this.flyout_) { this.workspaceDragger_ = new Blockly.FlyoutDragger(this.flyout_); } else { - this.workspaceDragger_ = new Blockly.WorkspaceDragger(this.startWorkspace_); + this.workspaceDragger_ = new Blockly.WorkspaceDragger( + /** @type {!Blockly.WorkspaceSvg} */ (this.startWorkspace_)); } this.isDraggingWorkspace_ = true; @@ -447,8 +444,9 @@ Blockly.Gesture.prototype.updateIsDragging_ = function() { * @private */ Blockly.Gesture.prototype.startDraggingBlock_ = function() { - this.blockDragger_ = new Blockly.BlockDragger(this.targetBlock_, - this.startWorkspace_); + this.blockDragger_ = new Blockly.BlockDragger( + /** @type {!Blockly.BlockSvg} */ (this.targetBlock_), + /** @type {!Blockly.WorkspaceSvg} */ (this.startWorkspace_)); this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_, this.healStack_); this.blockDragger_.dragBlock(this.mostRecentEvent_, this.currentDragDeltaXY_); @@ -460,8 +458,9 @@ Blockly.Gesture.prototype.startDraggingBlock_ = function() { */ // TODO (fenichel): Possibly combine this and startDraggingBlock_. Blockly.Gesture.prototype.startDraggingBubble_ = function() { - this.bubbleDragger_ = new Blockly.BubbleDragger(this.startBubble_, - this.startWorkspace_); + this.bubbleDragger_ = new Blockly.BubbleDragger( + /** @type {!Blockly.Bubble} */ (this.startBubble_), + /** @type {!Blockly.WorkspaceSvg} */ (this.startWorkspace_)); this.bubbleDragger_.startBubbleDrag(); this.bubbleDragger_.dragBubble(this.mostRecentEvent_, this.currentDragDeltaXY_); @@ -486,17 +485,20 @@ Blockly.Gesture.prototype.doStart = function(e) { // dragged, the block was moved, the parent workspace zoomed, etc. this.startWorkspace_.resize(); } - this.startWorkspace_.markFocused(); - this.mostRecentEvent_ = e; // Hide chaff also hides the flyout, so don't do it if the click is in a // flyout. Blockly.hideChaff(!!this.flyout_); + + this.startWorkspace_.markFocused(); + this.mostRecentEvent_ = e; + Blockly.Tooltip.block(); if (this.targetBlock_) { - if (!this.targetBlock_.isInFlyout && e.shiftKey) { - Blockly.navigation.enableKeyboardAccessibility(); + if (!this.targetBlock_.isInFlyout && + e.shiftKey && + this.targetBlock_.workspace.keyboardAccessibilityMode) { this.creatorWorkspace_.getCursor().setCurNode( Blockly.navigation.getTopNode(this.targetBlock_)); } else { @@ -512,7 +514,7 @@ Blockly.Gesture.prototype.doStart = function(e) { if ((e.type.toLowerCase() == 'touchstart' || e.type.toLowerCase() == 'pointerdown') && e.pointerType != 'mouse') { - Blockly.longStart_(e, this); + Blockly.longStart(e, this); } this.mouseDownXY_ = new Blockly.utils.Coordinate(e.clientX, e.clientY); @@ -630,13 +632,13 @@ Blockly.Gesture.prototype.cancel = function() { Blockly.Gesture.prototype.handleRightClick = function(e) { if (this.targetBlock_) { this.bringBlockToFront_(); - Blockly.hideChaff(this.flyout_); - this.targetBlock_.showContextMenu_(e); + Blockly.hideChaff(!!this.flyout_); + this.targetBlock_.showContextMenu(e); } else if (this.startBubble_) { - this.startBubble_.showContextMenu_(e); + this.startBubble_.showContextMenu(e); } else if (this.startWorkspace_ && !this.flyout_) { Blockly.hideChaff(); - this.startWorkspace_.showContextMenu_(e); + this.startWorkspace_.showContextMenu(e); } // TODO: Handle right-click on a bubble. @@ -649,7 +651,7 @@ Blockly.Gesture.prototype.handleRightClick = function(e) { /** * Handle a mousedown/touchstart event on a workspace. * @param {!Event} e A mouse down or touch start event. - * @param {!Blockly.Workspace} ws The workspace the event hit. + * @param {!Blockly.WorkspaceSvg} ws The workspace the event hit. * @package */ Blockly.Gesture.prototype.handleWsStart = function(e, ws) { @@ -660,7 +662,7 @@ Blockly.Gesture.prototype.handleWsStart = function(e, ws) { this.setStartWorkspace_(ws); this.mostRecentEvent_ = e; this.doStart(e); - if (Blockly.keyboardAccessibilityMode) { + if (this.startWorkspace_.keyboardAccessibilityMode) { Blockly.navigation.setState(Blockly.navigation.STATE_WS); } }; @@ -729,7 +731,7 @@ Blockly.Gesture.prototype.doBubbleClick_ = function() { * @private */ Blockly.Gesture.prototype.doFieldClick_ = function() { - this.startField_.showEditor_(); + this.startField_.showEditor(this.mostRecentEvent_); this.bringBlockToFront_(); }; @@ -764,8 +766,7 @@ Blockly.Gesture.prototype.doBlockClick_ = function() { */ Blockly.Gesture.prototype.doWorkspaceClick_ = function(e) { var ws = this.creatorWorkspace_; - if (e.shiftKey) { - Blockly.navigation.enableKeyboardAccessibility(); + if (e.shiftKey && ws.keyboardAccessibilityMode) { var screenCoord = new Blockly.utils.Coordinate(e.clientX, e.clientY); var wsCoord = Blockly.utils.screenToWsCoordinates(ws, screenCoord); var wsNode = Blockly.ASTNode.createWorkspaceNode(ws, wsCoord); @@ -975,7 +976,7 @@ Blockly.Gesture.prototype.getInsertionMarkers = function() { */ Blockly.Gesture.inProgress = function() { var workspaces = Blockly.Workspace.getAll(); - for (var i = 0, workspace; workspace = workspaces[i]; i++) { + for (var i = 0, workspace; (workspace = workspaces[i]); i++) { if (workspace.currentGesture_) { return true; } diff --git a/core/grid.js b/core/grid.js index 3311264c4..72367c10b 100644 --- a/core/grid.js +++ b/core/grid.js @@ -64,14 +64,15 @@ Blockly.Grid = function(pattern, options) { * @type {SVGElement} * @private */ - this.line1_ = pattern.firstChild; + this.line1_ = /** @type {SVGElement} */ (pattern.firstChild); /** * The vertical grid line, if it exists. * @type {SVGElement} * @private */ - this.line2_ = this.line1_ && this.line1_.nextSibling; + this.line2_ = this.line1_ && + (/** @type {SVGElement} */ (this.line1_.nextSibling)); /** * Whether blocks should snap to the grid. @@ -92,6 +93,7 @@ Blockly.Grid.prototype.scale_ = 1; /** * Dispose of this grid and unlink from the DOM. * @package + * @suppress {checkTypes} */ Blockly.Grid.prototype.dispose = function() { this.gridPattern_ = null; @@ -153,7 +155,7 @@ Blockly.Grid.prototype.update = function(scale) { /** * Set the attributes on one of the lines in the grid. Use this to update the * length and stroke width of the grid lines. - * @param {!SVGElement} line Which line to update. + * @param {SVGElement} line Which line to update. * @param {number} width The new stroke size (in px). * @param {number} x1 The new x start position of the line (in px). * @param {number} x2 The new x end position of the line (in px). diff --git a/core/icon.js b/core/icon.js index cd76e888c..5fb7cf9c7 100644 --- a/core/icon.js +++ b/core/icon.js @@ -139,9 +139,9 @@ Blockly.Icon.prototype.iconClick_ = function(e) { /** * Change the colour of the associated bubble to match its block. */ -Blockly.Icon.prototype.updateColour = function() { +Blockly.Icon.prototype.applyColour = function() { if (this.isVisible()) { - this.bubble_.setColour(this.block_.getColour()); + this.bubble_.setColour(this.block_.style.colourPrimary); } }; @@ -174,8 +174,8 @@ Blockly.Icon.prototype.computeIconLocation = function() { /** * Returns the center of the block's icon relative to the surface. - * @return {!Blockly.utils.Coordinate} Object with x and y properties in workspace - * coordinates. + * @return {Blockly.utils.Coordinate} Object with x and y properties in + * workspace coordinates. */ Blockly.Icon.prototype.getIconLocation = function() { return this.iconXY_; diff --git a/core/inject.js b/core/inject.js index 74c755fa2..cade0f695 100644 --- a/core/inject.js +++ b/core/inject.js @@ -29,9 +29,11 @@ goog.require('Blockly.Css'); goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Events'); goog.require('Blockly.Grid'); +goog.require('Blockly.Msg'); goog.require('Blockly.Options'); goog.require('Blockly.ScrollbarPair'); goog.require('Blockly.Tooltip'); +goog.require('Blockly.user.keyMap'); goog.require('Blockly.utils'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.userAgent'); @@ -41,10 +43,10 @@ goog.require('Blockly.WorkspaceSvg'); /** * Inject a Blockly editor into the specified container element (usually a div). - * @param {!Element|string} container Containing element, or its ID, + * @param {Element|string} container Containing element, or its ID, * or a CSS selector. - * @param {Object=} opt_options Optional dictionary of options. - * @return {!Blockly.Workspace} Newly created main workspace. + * @param {Blockly.BlocklyOptions=} opt_options Optional dictionary of options. + * @return {!Blockly.WorkspaceSvg} Newly created main workspace. */ Blockly.inject = function(container, opt_options) { Blockly.checkBlockColourConstants(); @@ -54,12 +56,17 @@ Blockly.inject = function(container, opt_options) { document.querySelector(container); } // Verify that the container is in document. - if (!Blockly.utils.dom.containsNode(document, container)) { + if (!container || !Blockly.utils.dom.containsNode(document, container)) { throw Error('Error: container is not in current document.'); } - var options = new Blockly.Options(opt_options || {}); + var options = new Blockly.Options(opt_options || + (/** @type {!Blockly.BlocklyOptions} */ ({}))); var subContainer = document.createElement('div'); subContainer.className = 'injectionDiv'; + subContainer.tabIndex = 0; + Blockly.utils.aria.setState(subContainer, + Blockly.utils.aria.State.LABEL, Blockly.Msg['WORKSPACE_ARIA_LABEL']); + container.appendChild(subContainer); var svg = Blockly.createDom_(subContainer, options); @@ -73,10 +80,16 @@ Blockly.inject = function(container, opt_options) { Blockly.user.keyMap.setKeyMap(options.keyMap); Blockly.init_(workspace); + + // Keep focus on the first workspace so entering keyboard navigation looks correct. Blockly.mainWorkspace = workspace; Blockly.svgResize(workspace); + subContainer.addEventListener('focusin', function() { + Blockly.mainWorkspace = workspace; + }); + return workspace; }; @@ -114,7 +127,8 @@ Blockly.createDom_ = function(container, options) { 'xmlns:html': Blockly.utils.dom.HTML_NS, 'xmlns:xlink': Blockly.utils.dom.XLINK_NS, 'version': '1.1', - 'class': 'blocklySvg' + 'class': 'blocklySvg', + 'tabindex': '0' }, container); /* @@ -126,73 +140,6 @@ Blockly.createDom_ = function(container, options) { // instances on a page. Browser behaviour becomes undefined otherwise. // https://neil.fraser.name/news/2015/11/01/ var rnd = String(Math.random()).substring(2); - /* - - - - - - - - - */ - var embossFilter = Blockly.utils.dom.createSvgElement('filter', - {'id': 'blocklyEmbossFilter' + rnd}, defs); - Blockly.utils.dom.createSvgElement('feGaussianBlur', - {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter); - var feSpecularLighting = Blockly.utils.dom.createSvgElement('feSpecularLighting', - { - 'in': 'blur', - 'surfaceScale': 1, - 'specularConstant': 0.5, - 'specularExponent': 10, - 'lighting-color': 'white', - 'result': 'specOut' - }, - embossFilter); - Blockly.utils.dom.createSvgElement('fePointLight', - {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting); - Blockly.utils.dom.createSvgElement('feComposite', - { - 'in': 'specOut', - 'in2': 'SourceAlpha', - 'operator': 'in', - 'result': 'specOut' - }, embossFilter); - Blockly.utils.dom.createSvgElement('feComposite', - { - 'in': 'SourceGraphic', - 'in2': 'specOut', - 'operator': 'arithmetic', - 'k1': 0, - 'k2': 1, - 'k3': 1, - 'k4': 0 - }, embossFilter); - options.embossFilterId = embossFilter.id; - /* - - - - - */ - var disabledPattern = Blockly.utils.dom.createSvgElement('pattern', - { - 'id': 'blocklyDisabledPattern' + rnd, - 'patternUnits': 'userSpaceOnUse', - 'width': 10, - 'height': 10 - }, defs); - Blockly.utils.dom.createSvgElement('rect', - {'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern); - Blockly.utils.dom.createSvgElement('path', - {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern); - options.disabledPatternId = disabledPattern.id; options.gridPattern = Blockly.Grid.createDom(rnd, options.gridOptions, defs); return svg; @@ -206,7 +153,7 @@ Blockly.createDom_ = function(container, options) { * for the blocks. * @param {!Blockly.WorkspaceDragSurfaceSvg} workspaceDragSurface Drag surface * SVG for the workspace. - * @return {!Blockly.Workspace} Newly created main workspace. + * @return {!Blockly.WorkspaceSvg} Newly created main workspace. * @private */ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, @@ -214,28 +161,35 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, options.parentWorkspace = null; var mainWorkspace = new Blockly.WorkspaceSvg(options, blockDragSurface, workspaceDragSurface); - mainWorkspace.scale = options.zoomOptions.startScale; + var wsOptions = mainWorkspace.options; + mainWorkspace.scale = wsOptions.zoomOptions.startScale; svg.appendChild(mainWorkspace.createDom('blocklyMainBackground')); - if (!options.hasCategories && options.languageTree) { + // Set the theme name and renderer name onto the injection div. + Blockly.utils.dom.addClass(mainWorkspace.getInjectionDiv(), + (wsOptions.renderer || 'geras') + '-renderer'); + Blockly.utils.dom.addClass(mainWorkspace.getInjectionDiv(), + mainWorkspace.getTheme().name + '-theme'); + + if (!wsOptions.hasCategories && wsOptions.languageTree) { // Add flyout as an that is a sibling of the workspace svg. - var flyout = mainWorkspace.addFlyout_('svg'); + var flyout = mainWorkspace.addFlyout('svg'); Blockly.utils.dom.insertAfter(flyout, svg); } - if (options.hasTrashcan) { + if (wsOptions.hasTrashcan) { mainWorkspace.addTrashcan(); } - if (options.zoomOptions && options.zoomOptions.controls) { + if (wsOptions.zoomOptions && wsOptions.zoomOptions.controls) { mainWorkspace.addZoomControls(); } // Register the workspace svg as a UI component. - mainWorkspace.getThemeManager().subscribe(svg, 'workspace', 'background-color'); + mainWorkspace.getThemeManager().subscribe(svg, 'workspaceBackgroundColour', + 'background-color'); // A null translation will also apply the correct initial scale. mainWorkspace.translate(0, 0); - Blockly.mainWorkspace = mainWorkspace; - if (!options.readOnly && !mainWorkspace.isMovable()) { + if (!wsOptions.readOnly && !mainWorkspace.isMovable()) { // Helper function for the workspaceChanged callback. // TODO (#2300): Move metrics math back to the WorkspaceSvg. var getWorkspaceMetrics = function() { @@ -303,7 +257,9 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, case Blockly.Events.BLOCK_CREATE: case Blockly.Events.BLOCK_MOVE: var object = mainWorkspace.getBlockById(e.blockId); - object = object.getRootBlock(); + if (object) { + object = object.getRootBlock(); + } break; case Blockly.Events.COMMENT_CREATE: case Blockly.Events.COMMENT_MOVE: @@ -361,7 +317,7 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, object.moveBy(deltaX, deltaY); } if (e) { - if (!e.group) { + if (!e.group && object) { console.log('WARNING: Moved object in bounds but there was no' + ' event group. This may break undo.'); } @@ -385,7 +341,7 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, /** * Initialize Blockly with various handlers. - * @param {!Blockly.Workspace} mainWorkspace Newly created main workspace. + * @param {!Blockly.WorkspaceSvg} mainWorkspace Newly created main workspace. * @private */ Blockly.init_ = function(mainWorkspace) { @@ -393,7 +349,8 @@ Blockly.init_ = function(mainWorkspace) { var svg = mainWorkspace.getParentSvg(); // Suppress the browser's context menu. - Blockly.bindEventWithChecks_(svg.parentNode, 'contextmenu', null, + Blockly.bindEventWithChecks_( + /** @type {!Element} */ (svg.parentNode), 'contextmenu', null, function(e) { if (!Blockly.utils.isTargetInput(e)) { e.preventDefault(); @@ -411,13 +368,15 @@ Blockly.init_ = function(mainWorkspace) { Blockly.inject.bindDocumentEvents_(); if (options.languageTree) { - if (mainWorkspace.toolbox_) { - mainWorkspace.toolbox_.init(mainWorkspace); - } else if (mainWorkspace.flyout_) { + var toolbox = mainWorkspace.getToolbox(); + var flyout = mainWorkspace.getFlyout(true); + if (toolbox) { + toolbox.init(); + } else if (flyout) { // Build a fixed flyout with the root blocks. - mainWorkspace.flyout_.init(mainWorkspace); - mainWorkspace.flyout_.show(options.languageTree.childNodes); - mainWorkspace.flyout_.scrollToStart(); + flyout.init(mainWorkspace); + flyout.show(options.languageTree.childNodes); + flyout.scrollToStart(); } } @@ -457,13 +416,13 @@ Blockly.inject.bindDocumentEvents_ = function() { if (!Blockly.documentEventsBound_) { Blockly.bindEventWithChecks_(document, 'scroll', null, function() { var workspaces = Blockly.Workspace.getAll(); - for (var i = 0, workspace; workspace = workspaces[i]; i++) { + for (var i = 0, workspace; (workspace = workspaces[i]); i++) { if (workspace.updateInverseScreenCTM) { workspace.updateInverseScreenCTM(); } } }); - Blockly.bindEventWithChecks_(document, 'keydown', null, Blockly.onKeyDown_); + Blockly.bindEventWithChecks_(document, 'keydown', null, Blockly.onKeyDown); // longStop needs to run to stop the context menu from showing up. It // should run regardless of what other touch event handlers have run. Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_); @@ -473,7 +432,8 @@ Blockly.inject.bindDocumentEvents_ = function() { Blockly.bindEventWithChecks_(window, 'orientationchange', document, function() { // TODO (#397): Fix for multiple Blockly workspaces. - Blockly.svgResize(Blockly.getMainWorkspace()); + Blockly.svgResize(/** @type {!Blockly.WorkspaceSvg} */ + (Blockly.getMainWorkspace())); }); } } diff --git a/core/input.js b/core/input.js index 5a8691e30..b8fd2e944 100644 --- a/core/input.js +++ b/core/input.js @@ -144,7 +144,7 @@ Blockly.Input.prototype.insertFieldAt = function(index, field, opt_name) { * @throws {Error} if the field is not present. */ Blockly.Input.prototype.removeField = function(name) { - for (var i = 0, field; field = this.fieldRow[i]; i++) { + for (var i = 0, field; (field = this.fieldRow[i]); i++) { if (field.name === name) { field.dispose(); this.fieldRow.splice(i, 1); @@ -185,15 +185,15 @@ Blockly.Input.prototype.setVisible = function(visible) { this.visible_ = visible; var display = visible ? 'block' : 'none'; - for (var y = 0, field; field = this.fieldRow[y]; y++) { + for (var y = 0, field; (field = this.fieldRow[y]); y++) { field.setVisible(visible); } if (this.connection) { // Has a connection. if (visible) { - renderList = this.connection.unhideAll(); + renderList = this.connection.startTrackingAll(); } else { - this.connection.hideAll(); + this.connection.stopTrackingAll(); } var child = this.connection.targetBlock(); if (child) { @@ -211,7 +211,7 @@ Blockly.Input.prototype.setVisible = function(visible) { * @package */ Blockly.Input.prototype.markDirty = function() { - for (var y = 0, field; field = this.fieldRow[y]; y++) { + for (var y = 0, field; (field = this.fieldRow[y]); y++) { field.markDirty(); } }; @@ -258,9 +258,10 @@ Blockly.Input.prototype.init = function() { /** * Sever all links to this input. + * @suppress {checkTypes} */ Blockly.Input.prototype.dispose = function() { - for (var i = 0, field; field = this.fieldRow[i]; i++) { + for (var i = 0, field; (field = this.fieldRow[i]); i++) { field.dispose(); } if (this.connection) { diff --git a/core/insertion_marker_manager.js b/core/insertion_marker_manager.js index 62f46719c..92860e643 100644 --- a/core/insertion_marker_manager.js +++ b/core/insertion_marker_manager.js @@ -40,7 +40,7 @@ Blockly.InsertionMarkerManager = function(block) { /** * The top block in the stack being dragged. * Does not change during a drag. - * @type {!Blockly.Block} + * @type {!Blockly.BlockSvg} * @private */ this.topBlock_ = block; @@ -146,27 +146,19 @@ Blockly.InsertionMarkerManager = function(block) { * @package */ Blockly.InsertionMarkerManager.prototype.dispose = function() { - this.topBlock_ = null; - this.workspace_ = null; this.availableConnections_.length = 0; - this.closestConnection_ = null; - this.localConnection_ = null; Blockly.Events.disable(); try { if (this.firstMarker_) { this.firstMarker_.dispose(); - this.firstMarker_ = null; } if (this.lastMarker_) { this.lastMarker_.dispose(); - this.lastMarker_ = null; } } finally { Blockly.Events.enable(); } - - this.highlightedBlock_ = null; }; /** @@ -255,14 +247,14 @@ Blockly.InsertionMarkerManager.prototype.createMarkerBlock_ = function(sourceBlo Blockly.Events.disable(); try { var result = this.workspace_.newBlock(imType); - result.setInsertionMarker(true, sourceBlock.width); - result.setCollapsed(sourceBlock.isCollapsed()); + result.setInsertionMarker(true); if (sourceBlock.mutationToDom) { var oldMutationDom = sourceBlock.mutationToDom(); if (oldMutationDom) { result.domToMutation(oldMutationDom); } } + result.setCollapsed(sourceBlock.isCollapsed()); // 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. @@ -301,7 +293,7 @@ Blockly.InsertionMarkerManager.prototype.initAvailableConnections_ = function() if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) { available.push(lastOnStack); this.lastOnStack_ = lastOnStack; - this.lastMarker_ = this.createMarkerBlock_(lastOnStack.sourceBlock_); + this.lastMarker_ = this.createMarkerBlock_(lastOnStack.getSourceBlock()); } return available; }; @@ -332,8 +324,8 @@ Blockly.InsertionMarkerManager.prototype.shouldUpdatePreviews_ = function( this.localConnection_ == candidateLocal) { return false; } - var xDiff = this.localConnection_.x_ + dxy.x - this.closestConnection_.x_; - var yDiff = this.localConnection_.y_ + dxy.y - this.closestConnection_.y_; + var xDiff = this.localConnection_.x + dxy.x - this.closestConnection_.x; + var yDiff = this.localConnection_.y + dxy.y - this.closestConnection_.y; var curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff); // Slightly prefer the existing preview over a new preview. return !(candidateClosest && radius > curDistance - @@ -360,6 +352,7 @@ Blockly.InsertionMarkerManager.prototype.shouldUpdatePreviews_ = function( * in workspace units. * @return {!Object} An object containing a local connection, a closest * connection, and a radius. + * @private */ Blockly.InsertionMarkerManager.prototype.getCandidate_ = function(dxy) { var radius = this.getStartRadius_(); @@ -415,9 +408,9 @@ Blockly.InsertionMarkerManager.prototype.shouldReplace_ = function() { // Dragging a block over an existing block in an input. if (local.type == Blockly.OUTPUT_VALUE) { // Insert the dragged block into the stack if possible. - if (!closest.isConnected() || - Blockly.Connection.lastConnectionInRow_(this.topBlock_, - closest.targetConnection.getSourceBlock())) { + if (closest && + this.workspace_.getRenderer() + .shouldInsertDraggedBlock(this.topBlock_, closest)) { return false; // Insert. } // Otherwise replace the existing block and bump it out. @@ -426,7 +419,7 @@ Blockly.InsertionMarkerManager.prototype.shouldReplace_ = function() { // Connecting to a statement input of c-block is an insertion, even if that // c-block is terminal (e.g. forever). - if (local == local.sourceBlock_.getFirstStatementConnection()) { + if (local == local.getSourceBlock().getFirstStatementConnection()) { return false; // Insert. } @@ -489,7 +482,7 @@ Blockly.InsertionMarkerManager.prototype.maybeShowPreview_ = function(candidate) // Something went wrong and we're trying to connect to an invalid connection. if (closest == this.closestConnection_ || - closest.sourceBlock_.isInsertionMarker()) { + closest.getSourceBlock().isInsertionMarker()) { console.log('Trying to connect to an insertion marker'); return; } @@ -511,7 +504,9 @@ Blockly.InsertionMarkerManager.prototype.showPreview_ = function() { this.connectMarker_(); } // Also highlight the actual connection, as a nod to previous behaviour. - if (this.closestConnection_) { + if (this.closestConnection_ && this.closestConnection_.targetBlock() && + this.workspace_.getRenderer() + .shouldHighlightConnection(this.closestConnection_)) { this.closestConnection_.highlight(); } }; @@ -555,7 +550,9 @@ Blockly.InsertionMarkerManager.prototype.maybeHidePreview_ = function(candidate) * @private */ Blockly.InsertionMarkerManager.prototype.hidePreview_ = function() { - if (this.closestConnection_) { + if (this.closestConnection_ && this.closestConnection_.targetBlock() && + this.workspace_.getRenderer() + .shouldHighlightConnection(this.closestConnection_)) { this.closestConnection_.unhighlight(); } if (this.highlightingBlock_) { @@ -567,6 +564,7 @@ Blockly.InsertionMarkerManager.prototype.hidePreview_ = function() { /** * Add highlighting showing which block will be replaced. + * @private */ Blockly.InsertionMarkerManager.prototype.highlightBlock_ = function() { var closest = this.closestConnection_; @@ -575,15 +573,15 @@ Blockly.InsertionMarkerManager.prototype.highlightBlock_ = function() { this.highlightedBlock_ = closest.targetBlock(); closest.targetBlock().highlightForReplacement(true); } else if (local.type == Blockly.OUTPUT_VALUE) { - this.highlightedBlock_ = closest.sourceBlock_; - // TODO: remove? - closest.sourceBlock_.highlightShapeForInput(closest, true); + this.highlightedBlock_ = closest.getSourceBlock(); + closest.getSourceBlock().highlightShapeForInput(closest, true); } this.highlightingBlock_ = true; }; /** * Get rid of the highlighting marking the block that will be replaced. + * @private */ Blockly.InsertionMarkerManager.prototype.unhighlightBlock_ = function() { var closest = this.closestConnection_; @@ -610,7 +608,7 @@ Blockly.InsertionMarkerManager.prototype.disconnectMarker_ = function() { } var imConn = this.markerConnection_; - var imBlock = imConn.sourceBlock_; + var imBlock = imConn.getSourceBlock(); var markerNext = imBlock.nextConnection; var markerPrev = imBlock.previousConnection; var markerOutput = imBlock.outputConnection; @@ -628,7 +626,7 @@ Blockly.InsertionMarkerManager.prototype.disconnectMarker_ = function() { // Inside of a C-block, first statement connection. else if (imConn.type == Blockly.NEXT_STATEMENT && imConn != markerNext) { var innerConnection = imConn.targetConnection; - innerConnection.sourceBlock_.unplug(false); + innerConnection.getSourceBlock().unplug(false); var previousBlockNextConnection = markerPrev ? markerPrev.targetConnection : null; @@ -660,7 +658,7 @@ Blockly.InsertionMarkerManager.prototype.connectMarker_ = function() { var isLastInStack = this.lastOnStack_ && local == this.lastOnStack_; var imBlock = isLastInStack ? this.lastMarker_ : this.firstMarker_; - var imConn = imBlock.getMatchingConnection(local.sourceBlock_, local); + var imConn = imBlock.getMatchingConnection(local.getSourceBlock(), local); if (imConn == this.markerConnection_) { throw Error('Made it to connectMarker_ even though the marker isn\'t ' + @@ -673,11 +671,15 @@ Blockly.InsertionMarkerManager.prototype.connectMarker_ = function() { imBlock.rendered = true; imBlock.getSvgRoot().setAttribute('visibility', 'visible'); - // Position based on the calculated connection locations. - imBlock.positionNewBlock(imBlock, imConn, closest); + if (imConn && closest) { + // Position so that the existing block doesn't move. + imBlock.positionNearConnection(imConn, closest); + } + if (closest) { + // Connect() also renders the insertion marker. + imConn.connect(closest); + } - // Connect() also renders the insertion marker. - imConn.connect(closest); this.markerConnection_ = imConn; }; diff --git a/core/keyboard_nav/ast_node.js b/core/keyboard_nav/ast_node.js index 9f1ed6e9d..509759aaf 100644 --- a/core/keyboard_nav/ast_node.js +++ b/core/keyboard_nav/ast_node.js @@ -23,6 +23,8 @@ goog.provide('Blockly.ASTNode'); +goog.require('Blockly.utils.Coordinate'); + /** * Class for an AST node. @@ -30,7 +32,7 @@ goog.provide('Blockly.ASTNode'); * creating a node directly. * @param {string} type The type of the location. * Must be in Bockly.ASTNode.types. - * @param {Blockly.Block|Blockly.Connection|Blockly.Field|Blockly.Workspace} + * @param {!(Blockly.Block|Blockly.Connection|Blockly.Field|Blockly.Workspace)} * location The position in the AST. * @param {!Object=} opt_params Optional dictionary of options. * @constructor @@ -80,6 +82,12 @@ Blockly.ASTNode.types = { WORKSPACE: 'workspace' }; +/** + * True to navigate to all fields. False to only navigate to clickable fields. + * @type {boolean} + */ +Blockly.ASTNode.NAVIGATE_ALL_FIELDS = false; + /** * The default y offset to use when moving the cursor from a stack to the * workspace. @@ -107,10 +115,13 @@ Blockly.ASTNode.isConnectionType_ = function(type) { /** * Create an AST node pointing to a field. - * @param {!Blockly.Field} field The location of the AST node. - * @return {!Blockly.ASTNode} An AST node pointing to a field. + * @param {Blockly.Field} field The location of the AST node. + * @return {Blockly.ASTNode} An AST node pointing to a field. */ Blockly.ASTNode.createFieldNode = function(field) { + if (!field) { + return null; + } return new Blockly.ASTNode(Blockly.ASTNode.types.FIELD, field); }; @@ -144,10 +155,10 @@ Blockly.ASTNode.createConnectionNode = function(connection) { * Creates an AST node pointing to an input. Stores the input connection as the * location. * @param {Blockly.Input} input The input used to create an AST node. - * @return {!Blockly.ASTNode} An AST node pointing to a input. + * @return {Blockly.ASTNode} An AST node pointing to a input. */ Blockly.ASTNode.createInputNode = function(input) { - if (!input) { + if (!input || !input.connection) { return null; } return new Blockly.ASTNode(Blockly.ASTNode.types.INPUT, input.connection); @@ -155,22 +166,28 @@ Blockly.ASTNode.createInputNode = function(input) { /** * Creates an AST node pointing to a block. - * @param {!Blockly.Block} block The block used to create an AST node. - * @return {!Blockly.ASTNode} An AST node pointing to a block. + * @param {Blockly.Block} block The block used to create an AST node. + * @return {Blockly.ASTNode} An AST node pointing to a block. */ Blockly.ASTNode.createBlockNode = function(block) { + if (!block) { + return null; + } return new Blockly.ASTNode(Blockly.ASTNode.types.BLOCK, block); }; /** * Create an AST node of type stack. A stack, represented by its top block, is * the set of all blocks connected to a top block, including the top block. - * @param {!Blockly.Block} topBlock A top block has no parent and can be found + * @param {Blockly.Block} topBlock A top block has no parent and can be found * in the list returned by workspace.getTopBlocks(). - * @return {!Blockly.ASTNode} An AST node of type stack that points to the top + * @return {Blockly.ASTNode} An AST node of type stack that points to the top * block on the stack. */ Blockly.ASTNode.createStackNode = function(topBlock) { + if (!topBlock) { + return null; + } return new Blockly.ASTNode(Blockly.ASTNode.types.STACK, topBlock); }; @@ -179,10 +196,13 @@ Blockly.ASTNode.createStackNode = function(topBlock) { * @param {!Blockly.Workspace} workspace The workspace that we are on. * @param {Blockly.utils.Coordinate} wsCoordinate The position on the workspace * for this node. - * @return {!Blockly.ASTNode} An AST node pointing to a workspace and a position + * @return {Blockly.ASTNode} An AST node pointing to a workspace and a position * on the workspace. */ Blockly.ASTNode.createWorkspaceNode = function(workspace, wsCoordinate) { + if (!wsCoordinate || !workspace) { + return null; + } var params = { wsCoordinate: wsCoordinate }; @@ -242,33 +262,6 @@ Blockly.ASTNode.prototype.isConnection = function() { return this.isConnection_; }; -/** - * Get either the previous editable field, or get the first editable field for - * the given input. - * @param {!(Blockly.Field|Blockly.Connection)} location The current location of - * the cursor, which must be a field or connection. - * @param {!Blockly.Input} parentInput The parentInput of the field. - * @param {boolean=} opt_last If true find the last editable field otherwise get - * the previous field. - * @return {Blockly.ASTNode} The AST node holding the previous or last field or - * null if no previous field exists. - * @private - */ -Blockly.ASTNode.prototype.findPreviousEditableField_ = function(location, - parentInput, opt_last) { - var fieldRow = parentInput.fieldRow; - var fieldIdx = fieldRow.indexOf(location); - var previousField = null; - var startIdx = (opt_last ? fieldRow.length : fieldIdx) - 1; - for (var i = startIdx, field; field = fieldRow[i]; i--) { - if (field.EDITABLE) { - previousField = field; - return Blockly.ASTNode.createFieldNode(previousField); - } - } - return null; -}; - /** * Given an input find the next editable field or an input with a non null * connection in the same block. The current location must be an input @@ -282,10 +275,10 @@ Blockly.ASTNode.prototype.findNextForInput_ = function() { var parentInput = this.location_.getParentInput(); var block = parentInput.getSourceBlock(); var curIdx = block.inputList.indexOf(parentInput); - for (var i = curIdx + 1, input; input = block.inputList[i]; i++) { + for (var i = curIdx + 1, input; (input = block.inputList[i]); i++) { var fieldRow = input.fieldRow; - for (var j = 0, field; field = fieldRow[j]; j++) { - if (field.EDITABLE) { + for (var j = 0, field; (field = fieldRow[j]); j++) { + if (field.isClickable() || Blockly.ASTNode.NAVIGATE_ALL_FIELDS) { return Blockly.ASTNode.createFieldNode(field); } } @@ -305,15 +298,15 @@ Blockly.ASTNode.prototype.findNextForInput_ = function() { * @private */ Blockly.ASTNode.prototype.findNextForField_ = function() { - var location = this.location_; + var location = /** @type {!Blockly.Field} */ (this.location_); var input = location.getParentInput(); var block = location.getSourceBlock(); - var curIdx = block.inputList.indexOf(input); + var curIdx = block.inputList.indexOf(/** @type {!Blockly.Input} */ (input)); var fieldIdx = input.fieldRow.indexOf(location) + 1; - for (var i = curIdx, newInput; newInput = block.inputList[i]; i++) { + for (var i = curIdx, newInput; (newInput = block.inputList[i]); i++) { var fieldRow = newInput.fieldRow; while (fieldIdx < fieldRow.length) { - if (fieldRow[fieldIdx].EDITABLE) { + if (fieldRow[fieldIdx].isClickable() || Blockly.ASTNode.NAVIGATE_ALL_FIELDS) { return Blockly.ASTNode.createFieldNode(fieldRow[fieldIdx]); } fieldIdx++; @@ -338,13 +331,13 @@ Blockly.ASTNode.prototype.findPrevForInput_ = function() { var location = this.location_.getParentInput(); var block = location.getSourceBlock(); var curIdx = block.inputList.indexOf(location); - for (var i = curIdx, input; input = block.inputList[i]; i--) { + for (var i = curIdx, input; (input = block.inputList[i]); i--) { if (input.connection && input !== location) { return Blockly.ASTNode.createInputNode(input); } var fieldRow = input.fieldRow; - for (var j = fieldRow.length - 1, field; field = fieldRow[j]; j--) { - if (field.EDITABLE) { + for (var j = fieldRow.length - 1, field; (field = fieldRow[j]); j--) { + if (field.isClickable() || Blockly.ASTNode.NAVIGATE_ALL_FIELDS) { return Blockly.ASTNode.createFieldNode(field); } } @@ -359,18 +352,19 @@ Blockly.ASTNode.prototype.findPrevForInput_ = function() { * @private */ Blockly.ASTNode.prototype.findPrevForField_ = function() { - var location = this.location_; + var location = /** @type {!Blockly.Field} */ (this.location_); var parentInput = location.getParentInput(); var block = location.getSourceBlock(); - var curIdx = block.inputList.indexOf(parentInput); + var curIdx = block.inputList.indexOf( + /** @type {!Blockly.Input} */ (parentInput)); var fieldIdx = parentInput.fieldRow.indexOf(location) - 1; - for (var i = curIdx, input; input = block.inputList[i]; i--) { + for (var i = curIdx, input; (input = block.inputList[i]); i--) { if (input.connection && input !== parentInput) { return Blockly.ASTNode.createInputNode(input); } var fieldRow = input.fieldRow; while (fieldIdx > -1) { - if (fieldRow[fieldIdx].EDITABLE) { + if (fieldRow[fieldIdx].isClickable() || Blockly.ASTNode.NAVIGATE_ALL_FIELDS) { return Blockly.ASTNode.createFieldNode(fieldRow[fieldIdx]); } fieldIdx--; @@ -400,7 +394,7 @@ Blockly.ASTNode.prototype.navigateBetweenStacks_ = function(forward) { } var curRoot = curLocation.getRootBlock(); var topBlocks = curRoot.workspace.getTopBlocks(true); - for (var i = 0, topBlock; topBlock = topBlocks[i]; i++) { + for (var i = 0, topBlock; (topBlock = topBlocks[i]); i++) { if (curRoot.id == topBlock.id) { var offset = forward ? 1 : -1; var resultIndex = i + offset; @@ -425,9 +419,11 @@ Blockly.ASTNode.prototype.navigateBetweenStacks_ = function(forward) { Blockly.ASTNode.prototype.findTopASTNodeForBlock_ = function(block) { var topConnection = block.previousConnection || block.outputConnection; if (topConnection) { - return Blockly.ASTNode.createConnectionNode(topConnection); + return /** @type {!Blockly.ASTNode} */ (Blockly.ASTNode.createConnectionNode( + topConnection)); } else { - return Blockly.ASTNode.createBlockNode(block); + return /** @type {!Blockly.ASTNode} */ (Blockly.ASTNode.createBlockNode( + block)); } }; @@ -469,10 +465,10 @@ Blockly.ASTNode.prototype.getOutAstNodeForBlock_ = function(block) { */ Blockly.ASTNode.prototype.findFirstFieldOrInput_ = function(block) { var inputs = block.inputList; - for (var i = 0, input; input = inputs[i]; i++) { + for (var i = 0, input; (input = inputs[i]); i++) { var fieldRow = input.fieldRow; - for (var j = 0, field; field = fieldRow[j]; j++) { - if (field.EDITABLE) { + for (var j = 0, field; (field = fieldRow[j]); j++) { + if (field.isClickable() || Blockly.ASTNode.NAVIGATE_ALL_FIELDS) { return Blockly.ASTNode.createFieldNode(field); } } @@ -483,6 +479,23 @@ Blockly.ASTNode.prototype.findFirstFieldOrInput_ = function(block) { return null; }; +/** + * Finds the source block of the location of this node. + * @return {Blockly.Block} The source block of the location, or null if the node + * is of type workspace. + */ +Blockly.ASTNode.prototype.getSourceBlock = function() { + if (this.getType() === Blockly.ASTNode.types.BLOCK) { + return /** @type {Blockly.Block} */ (this.getLocation()); + } else if (this.getType() === Blockly.ASTNode.types.STACK) { + return /** @type {Blockly.Block} */ (this.getLocation()); + } else if (this.getType() === Blockly.ASTNode.types.WORKSPACE) { + return null; + } else { + return this.getLocation().getSourceBlock(); + } +}; + /** * Find the element to the right of the current element in the AST. * @return {Blockly.ASTNode} An AST node that wraps the next field, connection, @@ -504,20 +517,14 @@ Blockly.ASTNode.prototype.next = function() { case Blockly.ASTNode.types.BLOCK: var nextConnection = this.location_.nextConnection; - if (nextConnection) { - return Blockly.ASTNode.createConnectionNode(nextConnection); - } - break; + return Blockly.ASTNode.createConnectionNode(nextConnection); case Blockly.ASTNode.types.PREVIOUS: return Blockly.ASTNode.createBlockNode(this.location_.getSourceBlock()); case Blockly.ASTNode.types.NEXT: var targetConnection = this.location_.targetConnection; - if (targetConnection) { - return Blockly.ASTNode.createConnectionNode(targetConnection); - } - break; + return Blockly.ASTNode.createConnectionNode(targetConnection); } return null; @@ -544,14 +551,11 @@ Blockly.ASTNode.prototype.in = function() { case Blockly.ASTNode.types.BLOCK: var block = /** @type {!Blockly.Block} */ (this.location_); - return this.findFirstFieldOrInput_(this.location_); + return this.findFirstFieldOrInput_(block); case Blockly.ASTNode.types.INPUT: var targetConnection = this.location_.targetConnection; - if (targetConnection) { - return Blockly.ASTNode.createConnectionNode(targetConnection); - } - break; + return Blockly.ASTNode.createConnectionNode(targetConnection); } return null; @@ -578,13 +582,9 @@ Blockly.ASTNode.prototype.prev = function() { return this.findPrevForInput_(); case Blockly.ASTNode.types.BLOCK: - var prevConnection = this.location_.previousConnection; - var outputConnection = this.location_.outputConnection; - var topConnection = prevConnection || outputConnection; - if (topConnection) { - return Blockly.ASTNode.createConnectionNode(topConnection); - } - break; + var block = this.location_; + var topConnection = block.previousConnection || block.outputConnection; + return Blockly.ASTNode.createConnectionNode(topConnection); case Blockly.ASTNode.types.PREVIOUS: var targetConnection = this.location_.targetConnection; diff --git a/demos/keyboard_nav/basic_cursor.js b/core/keyboard_nav/basic_cursor.js similarity index 85% rename from demos/keyboard_nav/basic_cursor.js rename to core/keyboard_nav/basic_cursor.js index feaa53ea2..d301a2fb1 100644 --- a/demos/keyboard_nav/basic_cursor.js +++ b/core/keyboard_nav/basic_cursor.js @@ -22,6 +22,11 @@ */ 'use strict'; +goog.provide('Blockly.BasicCursor'); + +goog.require('Blockly.ASTNode'); +goog.require('Blockly.Cursor'); + /** * Class for a basic cursor. @@ -35,12 +40,131 @@ Blockly.BasicCursor = function() { }; Blockly.utils.object.inherits(Blockly.BasicCursor, Blockly.Cursor); +/** + * Find the next node in the pre order traversal. + * @return {Blockly.ASTNode} The next node, or null if the current node is + * not set or there is no next value. + * @override + */ +Blockly.BasicCursor.prototype.next = function() { + var curNode = this.getCurNode(); + if (!curNode) { + return null; + } + var newNode = this.getNextNode_(curNode, this.validNode_); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; +}; + +/** + * For a basic cursor we only have the ability to go next and previous, so + * in will also allow the user to get to the next node in the pre order traversal. + * @return {Blockly.ASTNode} The next node, or null if the current node is + * not set or there is no next value. + * @override + */ +Blockly.BasicCursor.prototype.in = function() { + return this.next(); +}; + +/** + * Find the previous node in the pre order traversal. + * @return {Blockly.ASTNode} The previous node, or null if the current node + * is not set or there is no previous value. + * @override + */ +Blockly.BasicCursor.prototype.prev = function() { + var curNode = this.getCurNode(); + if (!curNode) { + return null; + } + var newNode = this.getPreviousNode_(curNode, this.validNode_); + + if (newNode) { + this.setCurNode(newNode); + } + return newNode; +}; + +/** + * For a basic cursor we only have the ability to go next and previou, so + * out will allow the user to get to the previous node in the pre order traversal. + * @return {Blockly.ASTNode} The previous node, or null if the current node is + * not set or there is no previous value. + * @override + */ +Blockly.BasicCursor.prototype.out = function() { + return this.prev(); +}; + +/** + * Uses pre order traversal to navigate the Blockly AST. This will allow + * a user to easily navigate the entire Blockly AST without having to go in + * and out levels on the tree. + * @param {Blockly.ASTNode} node The current position in the AST. + * @param {!function(Blockly.ASTNode) : boolean} isValid A function true/false + * depending on whether the given node should be traversed. + * @return {Blockly.ASTNode} The next node in the traversal. + * @protected + */ +Blockly.BasicCursor.prototype.getNextNode_ = function(node, isValid) { + if (!node) { + return null; + } + var newNode = node.in() || node.next(); + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getNextNode_(newNode, isValid); + } + var siblingOrParent = this.findSiblingOrParent_(node.out()); + if (isValid(siblingOrParent)) { + return siblingOrParent; + } else if (siblingOrParent) { + return this.getNextNode_(siblingOrParent, isValid); + } + return null; +}; + +/** + * Reverses the pre order traversal in order to find the previous node. This will + * allow a user to easily navigate the entire Blockly AST without having to go in + * and out levels on the tree. + * @param {Blockly.ASTNode} node The current position in the AST. + * @param {!function(Blockly.ASTNode) : boolean} isValid A function true/false + * depending on whether the given node should be traversed. + * @return {Blockly.ASTNode} The previous node in the traversal or null if no + * previous node exists. + * @protected + */ +Blockly.BasicCursor.prototype.getPreviousNode_ = function(node, isValid) { + if (!node) { + return null; + } + var newNode = node.prev(); + + if (newNode) { + newNode = this.getRightMostChild_(newNode); + } else { + newNode = node.out(); + } + if (isValid(newNode)) { + return newNode; + } else if (newNode) { + return this.getPreviousNode_(newNode, isValid); + } + return null; +}; + /** * Decides what nodes to traverse and which ones to skip. Currently, it * skips output, stack and workspace nodes. * @param {Blockly.ASTNode} node The AST node to check whether it is valid. * @return {boolean} True if the node should be visited, false otherwise. - * @private + * @protected */ Blockly.BasicCursor.prototype.validNode_ = function(node) { var isValid = false; @@ -74,32 +198,6 @@ Blockly.BasicCursor.prototype.findSiblingOrParent_ = function(node) { return this.findSiblingOrParent_(node.out()); }; -/** - * Uses pre order traversal to navigate the Blockly AST. This will allow - * a user to easily navigate the entire Blockly AST without having to go in - * and out levels on the tree. - * @param {Blockly.ASTNode} node The current position in the AST. - * @return {Blockly.ASTNode} The next node in the traversal. - * @private - */ -Blockly.BasicCursor.prototype.getNextNode_ = function(node) { - if (!node) { - return null; - } - var newNode = node.in() || node.next(); - if (this.validNode_(newNode)) { - return newNode; - } else if (newNode) { - return this.getNextNode_(newNode); - } - var siblingOrParent = this.findSiblingOrParent_(node.out()); - if (this.validNode_(siblingOrParent)) { - return siblingOrParent; - } else if (siblingOrParent) { - return this.getNextNode_(siblingOrParent); - } - return null; -}; /** * Get the right most child of a node. @@ -119,91 +217,3 @@ Blockly.BasicCursor.prototype.getRightMostChild_ = function(node) { return this.getRightMostChild_(newNode); }; - -/** - * Reverses the pre order traversal in order to find the previous node. This will - * allow a user to easily navigate the entire Blockly AST without having to go in - * and out levels on the tree. - * @param {Blockly.ASTNode} node The current position in the AST. - * @return {Blockly.ASTNode} The previous node in the traversal or null if no - * previous node exists. - * @private - */ -Blockly.BasicCursor.prototype.getPreviousNode_ = function(node) { - if (!node) { - return null; - } - var newNode = node.prev(); - - if (newNode) { - newNode = this.getRightMostChild_(newNode); - } else { - newNode = node.out(); - } - if (this.validNode_(newNode)) { - return newNode; - } else if (newNode) { - return this.getPreviousNode_(newNode); - } - return null; -}; - -/** - * Find the next node in the pre order traversal. - * @return {Blockly.ASTNode} The next node, or null if the current node is - * not set or there is no next value. - * @override - */ -Blockly.BasicCursor.prototype.next = function() { - var curNode = this.getCurNode(); - if (!curNode) { - return null; - } - var newNode = this.getNextNode_(curNode); - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; -}; - -/** - * For a basic cursor we only have the ability to go next and previous, so - * in will also allow the user to get to the next node in the pre order traversal. - * @return {Blockly.ASTNode} The next node, or null if the current node is - * not set or there is no next value. - * @override - */ -Blockly.BasicCursor.prototype.in = function() { - return this.next(); -}; - -/** - * Find the previous node in the pre order traversal. - * @return {Blockly.ASTNode} The previous node, or null if the current node - * is not set or there is no previous value. - * @override - */ -Blockly.BasicCursor.prototype.prev = function() { - var curNode = this.getCurNode(); - if (!curNode) { - return null; - } - var newNode = this.getPreviousNode_(curNode); - - if (newNode) { - this.setCurNode(newNode); - } - return newNode; -}; - -/** - * For a basic cursor we only have the ability to go next and previou, so - * out will allow the user to get to the previous node in the pre order traversal. - * @return {Blockly.ASTNode} The previous node, or null if the current node is - * not set or there is no previous value. - * @override - */ -Blockly.BasicCursor.prototype.out = function() { - return this.prev(); -}; diff --git a/core/keyboard_nav/cursor.js b/core/keyboard_nav/cursor.js index 01ed5b737..7342e2491 100644 --- a/core/keyboard_nav/cursor.js +++ b/core/keyboard_nav/cursor.js @@ -24,79 +24,34 @@ goog.provide('Blockly.Cursor'); +goog.require('Blockly.Action'); +goog.require('Blockly.ASTNode'); +goog.require('Blockly.Marker'); +goog.require('Blockly.navigation'); +goog.require('Blockly.utils.object'); + /** * Class for a cursor. * A cursor controls how a user navigates the Blockly AST. * @constructor + * @extends {Blockly.Marker} */ Blockly.Cursor = function() { - /* - * The current location of the cursor. - * @type {Blockly.ASTNode} - * @private - */ - this.curNode_ = null; + Blockly.Cursor.superClass_.constructor.call(this); /** - * The object in charge of drawing the visual representation of the current node. - * @type {Blockly.CursorSvg} - * @private + * @override */ - this.drawer_ = null; -}; - -/** - * Sets the object in charge of drawing the cursor. - * @param {Blockly.CursorSvg} drawer The object in charge of drawing the cursor. - */ -Blockly.Cursor.prototype.setDrawer = function(drawer) { - this.drawer_ = drawer; -}; - -/** - * Get the current drawer for the cursor. - * @return {Blockly.CursorSvg} The object in charge of drawing the cursor. - */ -Blockly.Cursor.prototype.getDrawer = function() { - return this.drawer_; -}; - -/** - * Gets the current location of the cursor. - * @return {Blockly.ASTNode} The current field, connection, or block the cursor - * is on. - */ -Blockly.Cursor.prototype.getCurNode = function() { - return this.curNode_; -}; - -/** - * Set the location of the cursor and call the update method. - * Setting isStack to true will only work if the newLocation is the top most - * output or previous connection on a stack. - * @param {Blockly.ASTNode} newNode The new location of the cursor. - */ -Blockly.Cursor.prototype.setCurNode = function(newNode) { - this.curNode_ = newNode; - if (this.drawer_) { - this.drawer_.draw(this.getCurNode()); - } -}; - -/** - * Hide the cursor SVG. - */ -Blockly.Cursor.prototype.hide = function() { - if (this.drawer_) { - this.drawer_.hide(); - } + this.type = 'cursor'; }; +Blockly.utils.object.inherits(Blockly.Cursor, Blockly.Marker); /** * Find the next connection, field, or block. * @return {Blockly.ASTNode} The next element, or null if the current node is * not set or there is no next value. + * @protected */ Blockly.Cursor.prototype.next = function() { var curNode = this.getCurNode(); @@ -121,6 +76,7 @@ Blockly.Cursor.prototype.next = function() { * Find the in connection or field. * @return {Blockly.ASTNode} The in element, or null if the current node is * not set or there is no in value. + * @protected */ Blockly.Cursor.prototype.in = function() { var curNode = this.getCurNode(); @@ -145,6 +101,7 @@ Blockly.Cursor.prototype.in = function() { * Find the previous connection, field, or block. * @return {Blockly.ASTNode} The previous element, or null if the current node * is not set or there is no previous value. + * @protected */ Blockly.Cursor.prototype.prev = function() { var curNode = this.getCurNode(); @@ -169,6 +126,7 @@ Blockly.Cursor.prototype.prev = function() { * Find the out connection, field, or block. * @return {Blockly.ASTNode} The out element, or null if the current node is * not set or there is no out value. + * @protected */ Blockly.Cursor.prototype.out = function() { var curNode = this.getCurNode(); @@ -186,3 +144,34 @@ Blockly.Cursor.prototype.out = function() { } return newNode; }; + +/** + * Handles the given action. + * This is only triggered when keyboard navigation is enabled. + * @param {!Blockly.Action} action The action to be handled. + * @return {boolean} True if the action has been handled, false otherwise. + */ +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)) { + return true; + } + switch (action.name) { + case Blockly.navigation.actionNames.PREVIOUS: + this.prev(); + return true; + case Blockly.navigation.actionNames.OUT: + this.out(); + return true; + case Blockly.navigation.actionNames.NEXT: + this.next(); + return true; + case Blockly.navigation.actionNames.IN: + this.in(); + return true; + default: + return false; + } +}; diff --git a/core/keyboard_nav/cursor_svg.js b/core/keyboard_nav/cursor_svg.js deleted file mode 100644 index 0151d4881..000000000 --- a/core/keyboard_nav/cursor_svg.js +++ /dev/null @@ -1,634 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Methods for graphically rendering a cursor as SVG. - * @author samelh@microsoft.com (Sam El-Husseini) - */ -'use strict'; - -goog.provide('Blockly.CursorSvg'); - -goog.require('Blockly.Cursor'); -goog.require('Blockly.utils.object'); - - -/** - * Class for a cursor. - * @param {!Blockly.WorkspaceSvg} workspace The workspace the cursor belongs to. - * @param {boolean=} opt_marker True if the cursor is a marker. A marker is used - * to save a location and is an immovable cursor. False or undefined if the - * cursor is not a marker. - * @constructor - */ -Blockly.CursorSvg = function(workspace, opt_marker) { - /** - * The workspace the cursor belongs to. - * @type {!Blockly.WorkspaceSvg} - * @private - */ - this.workspace_ = workspace; - - /** - * True if the cursor should be drawn as a marker, false otherwise. - * A marker is drawn as a solid blue line, while the cursor is drawns as a - * flashing red one. - * @type {boolean|undefined} - * @private - */ - this.isMarker_ = opt_marker; - - /** - * The workspace, field, or block that the cursor SVG element should be - * attached to. - * @type {Blockly.WorkspaceSvg|Blockly.Field|Blockly.BlockSvg} - * @private - */ - this.parent_ = null; - - /** - * The constants necessary to draw the cursor. - * @type {Blockly.blockRendering.ConstantProvider} - * @private - */ - this.constants_ = workspace.getRenderer().getConstants(); -}; - -/** - * Height of the horizontal cursor. - * @type {number} - * @const - */ -Blockly.CursorSvg.CURSOR_HEIGHT = 5; - -/** - * Width of the horizontal cursor. - * @type {number} - * @const - */ -Blockly.CursorSvg.CURSOR_WIDTH = 100; - -/** - * The start length of the notch. - * @type {number} - * @const - */ -Blockly.CursorSvg.NOTCH_START_LENGTH = 24; - -/** - * Padding around the input. - * @type {number} - * @const - */ -Blockly.CursorSvg.VERTICAL_PADDING = 5; - -/** - * Padding around a stack. - * @type {number} - * @const - */ -Blockly.CursorSvg.STACK_PADDING = 10; - -/** - * Padding around a block. - * @type {number} - * @const - */ -Blockly.CursorSvg.BLOCK_PADDING = 2; - -/** - * What we multiply the height by to get the height of the cursor. - * Only used for the block and block connections. - * @type {number} - * @const - */ -Blockly.CursorSvg.HEIGHT_MULTIPLIER = 3 / 4; - -/** - * Cursor color. - * @type {string} - * @const - */ -Blockly.CursorSvg.CURSOR_COLOR = '#cc0a0a'; - -/** - * Immovable marker color. - * @type {string} - * @const - */ -Blockly.CursorSvg.MARKER_COLOR = '#4286f4'; - -/** - * The name of the CSS class for a cursor. - * @const {string} - */ -Blockly.CursorSvg.CURSOR_CLASS = 'blocklyCursor'; - -/** - * The name of the CSS class for a marker. - * @const {string} - */ -Blockly.CursorSvg.MARKER_CLASS = 'blocklyMarker'; - -/** - * The current SVG element for the cursor. - * @type {Element} - */ -Blockly.CursorSvg.prototype.currentCursorSvg = null; - -/** - * Return the root node of the SVG or null if none exists. - * @return {SVGElement} The root SVG node. - */ -Blockly.CursorSvg.prototype.getSvgRoot = function() { - return this.svgGroup_; -}; - -/** - * Create the DOM element for the cursor. - * @return {!SVGElement} The cursor controls SVG group. - * @package - */ -Blockly.CursorSvg.prototype.createDom = function() { - var className = this.isMarker_ ? - Blockly.CursorSvg.MARKER_CLASS : Blockly.CursorSvg.CURSOR_CLASS; - - this.svgGroup_ = - Blockly.utils.dom.createSvgElement('g', { - 'class': className - }, null); - - this.createCursorSvg_(); - return this.svgGroup_; -}; - -/** - * Attaches the SVG root of the cursor to the SVG group of the parent. - * @param {!Blockly.WorkspaceSvg|!Blockly.Field|!Blockly.BlockSvg} newParent - * The workspace, field, or block that the cursor SVG element should be - * attached to. - * @private - */ -Blockly.CursorSvg.prototype.setParent_ = function(newParent) { - if (this.isMarker_) { - if (this.parent_) { - this.parent_.setMarkerSvg(null); - } - newParent.setMarkerSvg(this.getSvgRoot()); - } else { - if (this.parent_) { - this.parent_.setCursorSvg(null); - } - newParent.setCursorSvg(this.getSvgRoot()); - } - this.parent_ = newParent; -}; - -/************************** - * Display - **************************/ - -/** - * Show the cursor 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 cursor is currently on. - * @private - */ -Blockly.CursorSvg.prototype.showWithBlockPrevOutput_ = function(block) { - if (!block) { - return; - } - var width = block.width; - var height = block.height; - var cursorHeight = height * Blockly.CursorSvg.HEIGHT_MULTIPLIER; - var cursorOffset = Blockly.CursorSvg.BLOCK_PADDING; - - if (block.previousConnection) { - this.positionPrevious_(width, cursorOffset, cursorHeight); - } else if (block.outputConnection) { - this.positionOutput_(width, height); - } else { - this.positionBlock_(width, cursorOffset, cursorHeight); - } - - 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 cursor for. - * @private - */ -Blockly.CursorSvg.prototype.showWithCoordinates_ = function(curNode) { - var wsCoordinate = curNode.getWsCoordinate(); - var x = wsCoordinate.x; - var y = wsCoordinate.y; - - if (this.workspace_.RTL) { - x -= Blockly.CursorSvg.CURSOR_WIDTH; - } - - this.positionLine_(x, y, Blockly.CursorSvg.CURSOR_WIDTH); - this.setParent_(this.workspace_); - this.showCurrent_(); -}; - -/** - * Show the visual representation of a field. - * This is a box around the field. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the cursor for. - * @private - */ -Blockly.CursorSvg.prototype.showWithField_ = function(curNode) { - var field = /** @type {Blockly.Field} */ (curNode.getLocation()); - var width = field.getSize().width; - var height = field.getSize().height; - - this.positionRect_(0, 0, width, height); - this.setParent_(field); - this.showCurrent_(); -}; - -/** - * Show the visual representation of an input. - * This is a puzzle piece. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the cursor for. - * @private - */ -Blockly.CursorSvg.prototype.showWithInput_ = function(curNode) { - var connection = /** @type {Blockly.Connection} */ - (curNode.getLocation()); - var sourceBlock = /** @type {Blockly.BlockSvg} */ (connection.getSourceBlock()); - - this.positionInput_(connection); - this.setParent_(sourceBlock); - this.showCurrent_(); -}; - - -/** - * Show the visual representation of a next connection. - * This is a horizontal line. - * @param {!Blockly.ASTNode} curNode The node that we want to draw the cursor for. - * @private - */ -Blockly.CursorSvg.prototype.showWithNext_ = function(curNode) { - var connection = curNode.getLocation(); - var targetBlock = /** @type {Blockly.BlockSvg} */ (connection.getSourceBlock()); - var x = 0; - var y = connection.getOffsetInBlock().y; - var width = targetBlock.getHeightWidth().width; - if (this.workspace_.RTL) { - x = -width; - } - this.positionLine_(x, y, width); - this.setParent_(targetBlock); - this.showCurrent_(); -}; - -/** - * Show the visual representation of 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 cursor for. - * @private - */ -Blockly.CursorSvg.prototype.showWithStack_ = function(curNode) { - var block = /** @type {Blockly.BlockSvg} */ (curNode.getLocation()); - - // Gets the height and width of entire stack. - var heightWidth = block.getHeightWidth(); - - // Add padding so that being on a stack looks different than being on a block. - var width = heightWidth.width + Blockly.CursorSvg.STACK_PADDING; - var height = heightWidth.height + Blockly.CursorSvg.STACK_PADDING; - - // Shift the rectangle slightly to upper left so padding is equal on all sides. - var xPadding = -1 * Blockly.CursorSvg.STACK_PADDING / 2; - var yPadding = -1 * Blockly.CursorSvg.STACK_PADDING / 2; - - var x = xPadding; - var y = yPadding; - - if (this.workspace_.RTL) { - x = -(width + xPadding); - } - this.positionRect_(x, y, width, height); - this.setParent_(block); - this.showCurrent_(); -}; - -/** - * Show the current cursor. - * @private - */ -Blockly.CursorSvg.prototype.showCurrent_ = function() { - this.hide(); - this.currentCursorSvg.style.display = ''; -}; - -/************************** - * Position - **************************/ - -/** - * Position the cursor for a block. - * Displays an outline of the top half of a rectangle around a block. - * @param {number} width The width of the block. - * @param {number} cursorOffset The extra padding for around the block. - * @param {number} cursorHeight The height of the cursor. - */ -Blockly.CursorSvg.prototype.positionBlock_ = function(width, cursorOffset, cursorHeight) { - var cursorPath = Blockly.utils.svgPaths.moveBy(-1 * cursorOffset, cursorHeight) + - Blockly.utils.svgPaths.lineOnAxis('V', -1 * cursorOffset) + - Blockly.utils.svgPaths.lineOnAxis('H', width + cursorOffset * 2) + - Blockly.utils.svgPaths.lineOnAxis('V', cursorHeight); - this.cursorBlock_.setAttribute('d', cursorPath); - if (this.workspace_.RTL) { - this.flipRtl_(this.cursorBlock_); - } - this.currentCursorSvg = this.cursorBlock_; -}; - -/** - * Position the cursor for an input connection. - * Displays a filled in puzzle piece. - * @param {!Blockly.Connection} connection The connection to position cursor around. - * @private - */ -Blockly.CursorSvg.prototype.positionInput_ = function(connection) { - var x = connection.getOffsetInBlock().x; - var y = connection.getOffsetInBlock().y; - - var path = Blockly.utils.svgPaths.moveTo(0,0) + - this.constants_.PUZZLE_TAB.pathDown; - - this.cursorInput_.setAttribute('d', path); - this.cursorInput_.setAttribute('transform', - 'translate(' + x + ',' + y + ')' + (this.workspace_.RTL ? ' scale(-1 1)' : '')); - this.currentCursorSvg = this.cursorInput_; -}; - -/** - * Move and show the cursor at the specified coordinate in workspace units. - * Displays a horizontal line. - * @param {number} x The new x, in workspace units. - * @param {number} y The new y, in workspace units. - * @param {number} width The new width, in workspace units. - * @private - */ -Blockly.CursorSvg.prototype.positionLine_ = function(x, y, width) { - this.cursorSvgLine_.setAttribute('x', x); - this.cursorSvgLine_.setAttribute('y', y); - this.cursorSvgLine_.setAttribute('width', width); - this.currentCursorSvg = this.cursorSvgLine_; -}; - -/** - * Position the cursor for an output connection. - * Displays a puzzle outline and the top and bottom path. - * @param {number} width The width of the block. - * @param {number} height The height of the block. - * @private - */ -Blockly.CursorSvg.prototype.positionOutput_ = function(width, height) { - var cursorPath = Blockly.utils.svgPaths.moveBy(width, 0) + - Blockly.utils.svgPaths.lineOnAxis('h', -1 * (width - this.constants_.PUZZLE_TAB.width)) + - Blockly.utils.svgPaths.lineOnAxis('v', this.constants_.TAB_OFFSET_FROM_TOP) + - this.constants_.PUZZLE_TAB.pathDown + - Blockly.utils.svgPaths.lineOnAxis('V', height) + - Blockly.utils.svgPaths.lineOnAxis('H', width); - this.cursorBlock_.setAttribute('d', cursorPath); - if (this.workspace_.RTL) { - this.flipRtl_(this.cursorBlock_); - } - this.currentCursorSvg = this.cursorBlock_; -}; - -/** - * Position the cursor for a previous connection. - * Displays a half rectangle with a notch in the top to represent the previous - * connection. - * @param {number} width The width of the block. - * @param {number} cursorOffset The offset of the cursor from around the block. - * @param {number} cursorHeight The height of the cursor. - * @private - */ -Blockly.CursorSvg.prototype.positionPrevious_ = function(width, cursorOffset, cursorHeight) { - var cursorPath = Blockly.utils.svgPaths.moveBy(-1 * cursorOffset, cursorHeight) + - Blockly.utils.svgPaths.lineOnAxis('V', -1 * cursorOffset) + - Blockly.utils.svgPaths.lineOnAxis('H', this.constants_.NOTCH_OFFSET_LEFT) + - this.constants_.NOTCH.pathLeft + - Blockly.utils.svgPaths.lineOnAxis('H', width + cursorOffset * 2) + - Blockly.utils.svgPaths.lineOnAxis('V', cursorHeight); - this.cursorBlock_.setAttribute('d', cursorPath); - if (this.workspace_.RTL) { - this.flipRtl_(this.cursorBlock_); - } - this.currentCursorSvg = this.cursorBlock_; -}; - -/** - * Move and show the cursor at the specified coordinate in workspace units. - * Displays a filled in rectangle. - * @param {number} x The new x, in workspace units. - * @param {number} y The new y, in workspace units. - * @param {number} width The new width, in workspace units. - * @param {number} height The new height, in workspace units. - * @private - */ -Blockly.CursorSvg.prototype.positionRect_ = function(x, y, width, height) { - this.cursorSvgRect_.setAttribute('x', x); - this.cursorSvgRect_.setAttribute('y', y); - this.cursorSvgRect_.setAttribute('width', width); - this.cursorSvgRect_.setAttribute('height', height); - this.currentCursorSvg = this.cursorSvgRect_; -}; - -/** - * Flip the SVG paths in RTL. - * @param {!SVGElement} cursor The cursor that we want to flip. - * @private - */ -Blockly.CursorSvg.prototype.flipRtl_ = function(cursor) { - cursor.setAttribute('transform', 'scale(-1 1)'); -}; - -/** - * Hide the cursor. - * @package - */ -Blockly.CursorSvg.prototype.hide = function() { - this.cursorSvgLine_.style.display = 'none'; - this.cursorSvgRect_.style.display = 'none'; - this.cursorInput_.style.display = 'none'; - this.cursorBlock_.style.display = 'none'; -}; - -/** - * Update the cursor. - * @param {Blockly.ASTNode} curNode The node that we want to draw the cursor for. - * @package - */ -Blockly.CursorSvg.prototype.draw = function(curNode) { - if (!curNode) { - this.hide(); - return; - } - - 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); - } -}; - -/** - * Create the cursor SVG. - * @return {Element} The SVG node created. - * @private - */ -Blockly.CursorSvg.prototype.createCursorSvg_ = function() { - /* This markup will be generated and added to the .svgGroup_: - - - - - - */ - - var colour = this.isMarker_ ? Blockly.CursorSvg.MARKER_COLOR : - Blockly.CursorSvg.CURSOR_COLOR; - this.cursorSvg_ = Blockly.utils.dom.createSvgElement('g', - { - 'width': Blockly.CursorSvg.CURSOR_WIDTH, - 'height': Blockly.CursorSvg.CURSOR_HEIGHT - }, this.svgGroup_); - - // A horizontal line used to represent a workspace coordinate or next connection. - this.cursorSvgLine_ = Blockly.utils.dom.createSvgElement('rect', - { - 'x': '0', - 'y': '0', - 'fill': colour, - 'width': Blockly.CursorSvg.CURSOR_WIDTH, - 'height': Blockly.CursorSvg.CURSOR_HEIGHT, - 'style': 'display: none;' - }, - this.cursorSvg_); - - // A filled in rectangle used to represent a stack. - this.cursorSvgRect_ = Blockly.utils.dom.createSvgElement('rect', - { - 'class': 'blocklyVerticalCursor', - 'x': '0', - 'y': '0', - 'rx': '10', 'ry': '10', - 'style': 'display: none;', - 'stroke': colour - }, - this.cursorSvg_); - - // A filled in puzzle piece used to represent an input value. - this.cursorInput_ = Blockly.utils.dom.createSvgElement( - 'path', - { - 'width': Blockly.CursorSvg.CURSOR_WIDTH, - 'height': Blockly.CursorSvg.CURSOR_HEIGHT, - 'transform':'', - 'style':'display: none;', - 'fill': colour - }, - this.cursorSvg_); - - // A path used to repreesent a previous connection and a block, an output - // connection and a block, or a block. - this.cursorBlock_ = Blockly.utils.dom.createSvgElement( - 'path', - { - 'width': Blockly.CursorSvg.CURSOR_WIDTH, - 'height': Blockly.CursorSvg.CURSOR_HEIGHT, - 'transform':'', - 'style':'display: none;', - 'fill': 'none', - 'stroke': colour, - 'stroke-width': '4' - }, - this.cursorSvg_); - - // Markers and stack cursors don't blink. - if (!this.isMarker_) { - Blockly.utils.dom.createSvgElement('animate', - { - 'attributeType': 'XML', - 'attributeName': 'fill', - 'dur': '1s', - 'values': Blockly.CursorSvg.CURSOR_COLOR + ';transparent;transparent;', - 'repeatCount': 'indefinite' - }, - this.cursorSvgLine_); - - Blockly.utils.dom.createSvgElement('animate', - { - 'attributeType': 'XML', - 'attributeName': 'fill', - 'dur': '1s', - 'values': Blockly.CursorSvg.CURSOR_COLOR + ';transparent;transparent;', - 'repeatCount': 'indefinite' - }, - this.cursorInput_); - - Blockly.utils.dom.createSvgElement('animate', - { - 'attributeType': 'XML', - 'attributeName': 'stroke', - 'dur': '1s', - 'values': Blockly.CursorSvg.CURSOR_COLOR + ';transparent;transparent;', - 'repeatCount': 'indefinite' - }, - this.cursorBlock_); - } - - return this.cursorSvg_; -}; - -/** - * Dispose of this cursor. - * @package - */ -Blockly.CursorSvg.prototype.dispose = function() { - if (this.svgGroup_) { - Blockly.utils.dom.removeNode(this.svgGroup_); - } -}; diff --git a/core/keyboard_nav/flyout_cursor.js b/core/keyboard_nav/flyout_cursor.js index 7c296f422..9208a24fe 100644 --- a/core/keyboard_nav/flyout_cursor.js +++ b/core/keyboard_nav/flyout_cursor.js @@ -25,6 +25,7 @@ goog.provide('Blockly.FlyoutCursor'); goog.require('Blockly.Cursor'); +goog.require('Blockly.navigation'); goog.require('Blockly.utils.object'); @@ -39,6 +40,26 @@ Blockly.FlyoutCursor = function() { }; Blockly.utils.object.inherits(Blockly.FlyoutCursor, Blockly.Cursor); +/** + * Handles the given action. + * This is only triggered when keyboard navigation is enabled. + * @param {!Blockly.Action} action The action to be handled. + * @return {boolean} True if the action has been handled, false otherwise. + * @override + */ +Blockly.FlyoutCursor.prototype.onBlocklyAction = function(action) { + switch (action.name) { + case Blockly.navigation.actionNames.PREVIOUS: + this.prev(); + return true; + case Blockly.navigation.actionNames.NEXT: + this.next(); + return true; + default: + return false; + } +}; + /** * Find the next connection, field, or block. * @return {Blockly.ASTNode} The next element, or null if the current node is diff --git a/core/keyboard_nav/key_map.js b/core/keyboard_nav/key_map.js index 2491cbc5b..01e5cea49 100644 --- a/core/keyboard_nav/key_map.js +++ b/core/keyboard_nav/key_map.js @@ -24,13 +24,16 @@ goog.provide('Blockly.user.keyMap'); +// TODO: Fix circular dependency. +// goog.require('Blockly.navigation'); goog.require('Blockly.utils.KeyCodes'); goog.require('Blockly.utils.object'); /** * Holds the serialized key to key action mapping. - * @type {Object} + * @type {!Object} + * @private */ Blockly.user.keyMap.map_ = {}; @@ -50,7 +53,6 @@ Blockly.user.keyMap.modifierKeys = { * @param {string} keyCode The key code serialized by the serializeKeyEvent. * @param {!Blockly.Action} action The action to be executed when the keys * corresponding to the serialized key code is pressed. - * @package */ Blockly.user.keyMap.setActionForKey = function(keyCode, action) { var oldKey = Blockly.user.keyMap.getKeyByAction(action); @@ -63,9 +65,8 @@ Blockly.user.keyMap.setActionForKey = function(keyCode, action) { /** * Creates a new key map. - * @param {Object} keyMap The object holding the key + * @param {!Object} keyMap The object holding the key * to action mapping. - * @package */ Blockly.user.keyMap.setKeyMap = function(keyMap) { Blockly.user.keyMap.map_ = keyMap; @@ -75,7 +76,6 @@ Blockly.user.keyMap.setKeyMap = function(keyMap) { * Gets the current key map. * @return {Object} The object holding the key to * action mapping. - * @package */ Blockly.user.keyMap.getKeyMap = function() { var map = {}; @@ -88,7 +88,6 @@ Blockly.user.keyMap.getKeyMap = function() { * @param {string} keyCode The serialized key code. * @return {Blockly.Action|undefined} The action holding the function to * call when the given keyCode is used or undefined if no action exists. - * @package */ Blockly.user.keyMap.getActionByKeyCode = function(keyCode) { return Blockly.user.keyMap.map_[keyCode]; @@ -100,11 +99,10 @@ Blockly.user.keyMap.getActionByKeyCode = function(keyCode) { * the key. * @return {?string} The serialized key or null if the action does not have * a key mapping. - * @package */ Blockly.user.keyMap.getKeyByAction = function(action) { var keys = Object.keys(Blockly.user.keyMap.map_); - for (var i = 0, key; key = keys[i]; i++) { + for (var i = 0, key; (key = keys[i]); i++) { if (Blockly.user.keyMap.map_[key].name === action.name) { return key; } @@ -116,11 +114,12 @@ Blockly.user.keyMap.getKeyByAction = function(action) { * Serialize the key event. * @param {!Event} e A key up event holding the key code. * @return {string} A string containing the serialized key event. + * @package */ Blockly.user.keyMap.serializeKeyEvent = function(e) { var modifiers = Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys); var key = ''; - for (var i = 0, keyName; keyName = modifiers[i]; i++) { + for (var i = 0, keyName; (keyName = modifiers[i]); i++) { if (e.getModifierState(keyName)) { key += keyName; } @@ -129,6 +128,21 @@ Blockly.user.keyMap.serializeKeyEvent = function(e) { return key; }; +/** + * Checks whether any of the given modifiers are not valid. + * @param {!Array.} modifiers List of modifiers to be used with the key. + * @param {!Array.} validModifiers List of modifiers we support. + * @throws {Error} if the modifier is not in the valid modifiers list. + * @private + */ +Blockly.user.keyMap.checkModifiers_ = function(modifiers, validModifiers) { + for (var i = 0, modifier; (modifier = modifiers[i]); i++) { + if (validModifiers.indexOf(modifier) < 0) { + throw Error(modifier + ' is not a valid modifier key.'); + } + } +}; + /** * Create the serialized key code that will be used in the key map. * @param {number} keyCode Number code representing the key. @@ -139,11 +153,10 @@ Blockly.user.keyMap.serializeKeyEvent = function(e) { Blockly.user.keyMap.createSerializedKey = function(keyCode, modifiers) { var key = ''; var validModifiers = Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys); - for (var i = 0, keyName; keyName = modifiers[i]; i++) { - if (validModifiers.indexOf(keyName) > -1) { - key += keyName; - } else { - throw Error(keyName + ' is not a valid modifier key.'); + Blockly.user.keyMap.checkModifiers_(modifiers, validModifiers); + for (var i = 0, validModifier; (validModifier = validModifiers[i]); i++) { + if (modifiers.indexOf(validModifier) > -1) { + key += validModifier; } } key += keyCode; @@ -158,7 +171,16 @@ Blockly.user.keyMap.createSerializedKey = function(keyCode, modifiers) { Blockly.user.keyMap.createDefaultKeyMap = function() { var map = {}; var controlK = Blockly.user.keyMap.createSerializedKey( - Blockly.utils.KeyCodes.K, [Blockly.user.keyMap.modifierKeys.CONTROL]); + Blockly.utils.KeyCodes.K, [Blockly.user.keyMap.modifierKeys.CONTROL, + Blockly.user.keyMap.modifierKeys.SHIFT]); + var shiftW = Blockly.user.keyMap.createSerializedKey( + Blockly.utils.KeyCodes.W, [Blockly.user.keyMap.modifierKeys.SHIFT]); + var shiftA = Blockly.user.keyMap.createSerializedKey( + Blockly.utils.KeyCodes.A, [Blockly.user.keyMap.modifierKeys.SHIFT]); + var shiftS = Blockly.user.keyMap.createSerializedKey( + Blockly.utils.KeyCodes.S, [Blockly.user.keyMap.modifierKeys.SHIFT]); + var shiftD = Blockly.user.keyMap.createSerializedKey( + Blockly.utils.KeyCodes.D, [Blockly.user.keyMap.modifierKeys.SHIFT]); map[Blockly.utils.KeyCodes.W] = Blockly.navigation.ACTION_PREVIOUS; map[Blockly.utils.KeyCodes.A] = Blockly.navigation.ACTION_OUT; @@ -171,5 +193,9 @@ Blockly.user.keyMap.createDefaultKeyMap = function() { map[Blockly.utils.KeyCodes.E] = Blockly.navigation.ACTION_EXIT; map[Blockly.utils.KeyCodes.ESC] = Blockly.navigation.ACTION_EXIT; map[controlK] = Blockly.navigation.ACTION_TOGGLE_KEYBOARD_NAV; + map[shiftW] = Blockly.navigation.ACTION_MOVE_WS_CURSOR_UP; + map[shiftA] = Blockly.navigation.ACTION_MOVE_WS_CURSOR_LEFT; + map[shiftS] = Blockly.navigation.ACTION_MOVE_WS_CURSOR_DOWN; + map[shiftD] = Blockly.navigation.ACTION_MOVE_WS_CURSOR_RIGHT; return map; }; diff --git a/core/keyboard_nav/marker.js b/core/keyboard_nav/marker.js new file mode 100644 index 000000000..f70fa0eea --- /dev/null +++ b/core/keyboard_nav/marker.js @@ -0,0 +1,133 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The class representing a marker. + * Used primarily for keyboard navigation to show a marked location. + * @author aschmiedt@google.com (Abby Schmiedt) + */ +'use strict'; + +goog.provide('Blockly.Marker'); + +goog.require('Blockly.ASTNode'); +goog.require('Blockly.navigation'); + + +/** + * Class for a marker. + * This is used in keyboard navigation to save a location in the Blockly AST. + * @constructor + */ +Blockly.Marker = function() { + + /** + * The colour of the marker. + * @type {?string} + */ + this.colour = null; + + /** + * The current location of the marker. + * @type {Blockly.ASTNode} + * @private + */ + this.curNode_ = null; + + /** + * The object in charge of drawing the visual representation of the current node. + * @type {Blockly.blockRendering.MarkerSvg} + * @private + */ + this.drawer_ = null; + + /** + * The type of the marker. + * @type {string} + */ + this.type = 'marker'; +}; + +/** + * Sets the object in charge of drawing the marker. + * @param {Blockly.blockRendering.MarkerSvg} drawer The object in charge of + * drawing the marker. + */ +Blockly.Marker.prototype.setDrawer = function(drawer) { + this.drawer_ = drawer; +}; + +/** + * Get the current drawer for the marker. + * @return {Blockly.blockRendering.MarkerSvg} The object in charge of drawing + * the marker. + */ +Blockly.Marker.prototype.getDrawer = function() { + return this.drawer_; +}; + +/** + * Gets the current location of the marker. + * @return {Blockly.ASTNode} The current field, connection, or block the marker + * is on. + */ +Blockly.Marker.prototype.getCurNode = function() { + return this.curNode_; +}; + +/** + * Set the location of the marker and call the update method. + * Setting isStack to true will only work if the newLocation is the top most + * output or previous connection on a stack. + * @param {Blockly.ASTNode} newNode The new location of the marker. + */ +Blockly.Marker.prototype.setCurNode = function(newNode) { + var oldNode = this.curNode_; + this.curNode_ = newNode; + if (this.drawer_) { + this.drawer_.draw(oldNode, this.curNode_); + } +}; + +/** + * Redraw the current marker. + * @package + */ +Blockly.Marker.prototype.draw = function() { + if (this.drawer_) { + this.drawer_.draw(this.curNode_, this.curNode_); + } +}; + +/** + * Hide the marker SVG. + */ +Blockly.Marker.prototype.hide = function() { + if (this.drawer_) { + this.drawer_.hide(); + } +}; + +/** + * Dispose of this marker. + */ +Blockly.Marker.prototype.dispose = function() { + if (this.getDrawer()) { + this.getDrawer().dispose(); + } +}; + diff --git a/core/keyboard_nav/marker_cursor.js b/core/keyboard_nav/marker_cursor.js deleted file mode 100644 index 0150ca66f..000000000 --- a/core/keyboard_nav/marker_cursor.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview The class representing a cursor used for marking a location. - * Used primarily for keyboard navigation. - * @author aschmiedt@google.com (Abby Schmiedt) - */ -'use strict'; - -goog.provide('Blockly.MarkerCursor'); - -goog.require('Blockly.Cursor'); -goog.require('Blockly.utils.object'); - - -/** - * Class for a marker. - * This is used in keyboard navigation to save a location in the Blockly AST. - * @constructor - * @extends {Blockly.Cursor} - */ -Blockly.MarkerCursor = function() { - Blockly.MarkerCursor.superClass_.constructor.call(this); -}; -Blockly.utils.object.inherits(Blockly.MarkerCursor, Blockly.Cursor); - -/** - * This is a no-op since markers do not move. - * @return {null} Always null. - * @override - */ -Blockly.MarkerCursor.prototype.next = function() { - return null; -}; - -/** - * This is a no-op since markers do not move. - * @return {null} Always null. - * @override - */ -Blockly.MarkerCursor.prototype.in = function() { - return null; -}; - -/** - * This is a no-op since markers do not move. - * @return {null} Always null. - * @override - */ -Blockly.MarkerCursor.prototype.prev = function() { - return null; -}; - -/** - * This is a no-op since markers do not move. - * @return {null} Always null. - * @override - */ -Blockly.MarkerCursor.prototype.out = function() { - return null; -}; diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js index cbe189a53..a2b861b5a 100644 --- a/core/keyboard_nav/navigation.js +++ b/core/keyboard_nav/navigation.js @@ -45,21 +45,31 @@ Blockly.navigation.loggingCallback = null; /** * State indicating focus is currently on the flyout. * @type {number} + * @const */ Blockly.navigation.STATE_FLYOUT = 1; /** * State indicating focus is currently on the workspace. * @type {number} + * @const */ Blockly.navigation.STATE_WS = 2; /** * State indicating focus is currently on the toolbox. * @type {number} + * @const */ Blockly.navigation.STATE_TOOLBOX = 3; +/** + * The distance to move the cursor on the workspace. + * @type {number} + * @const + */ +Blockly.navigation.WS_MOVE_DISTANCE = 40; + /** * The current state the user is in. * Initialized to workspace state since a user enters navigation mode by shift @@ -83,17 +93,35 @@ Blockly.navigation.actionNames = { DISCONNECT: 'disconnect', TOOLBOX: 'toolbox', EXIT: 'exit', - TOGGLE_KEYBOARD_NAV: 'toggle_keyboard_nav' + 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' }; +/** + * The name of the marker reserved for internal use. + * @type {string} + * @const + */ +Blockly.navigation.MARKER_NAME = 'local_marker_1'; + /** ****** */ /** Focus */ /** ****** */ +/** + * Get the local marker. + * @return {!Blockly.Marker} The local marker for the main workspace. + */ +Blockly.navigation.getMarker = function() { + return Blockly.getMainWorkspace().getMarker(Blockly.navigation.MARKER_NAME); +}; + /** * If a toolbox exists, set the navigation state to toolbox and select the first * category in the toolbox. - * category. * @private */ Blockly.navigation.focusToolbox_ = function() { @@ -103,7 +131,7 @@ Blockly.navigation.focusToolbox_ = function() { Blockly.navigation.currentState_ = Blockly.navigation.STATE_TOOLBOX; Blockly.navigation.resetFlyout_(false /* shouldHide */); - if (!workspace.getMarker().getCurNode()) { + if (!Blockly.navigation.getMarker().getCurNode()) { Blockly.navigation.markAtCursor_(); } toolbox.selectFirstCategory(); @@ -121,7 +149,7 @@ Blockly.navigation.focusFlyout_ = function() { var toolbox = workspace.getToolbox(); var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout(); - if (!workspace.getMarker().getCurNode()) { + if (!Blockly.navigation.getMarker().getCurNode()) { Blockly.navigation.markAtCursor_(); } @@ -202,10 +230,10 @@ Blockly.navigation.insertFromFlyout = function() { var newBlock = flyout.createBlock(curBlock); // Render to get the sizing right. newBlock.render(); - // Connections are hidden when the block is first created. Normally there's - // enough time for them to become unhidden in the user's mouse movements, - // but not here. - newBlock.setConnectionsHidden(false); + // Connections are not tracked when the block is first created. Normally + // there's enough time for them to become tracked in the user's mouse + // movements, but not here. + newBlock.setConnectionTracking(true); workspace.getCursor().setCurNode( Blockly.ASTNode.createBlockNode(newBlock)); if (!Blockly.navigation.modify_()) { @@ -242,7 +270,7 @@ Blockly.navigation.resetFlyout_ = function(shouldHide) { * @private */ Blockly.navigation.modifyWarn_ = function() { - var markerNode = Blockly.getMainWorkspace().getMarker().getCurNode(); + var markerNode = Blockly.navigation.getMarker().getCurNode(); var cursorNode = Blockly.getMainWorkspace().getCursor().getCurNode(); if (!markerNode) { @@ -283,7 +311,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.Block} 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, @@ -291,6 +319,9 @@ Blockly.navigation.modifyWarn_ = function() { * @private */ Blockly.navigation.moveBlockToWorkspace_ = function(block, wsNode) { + if (!block) { + return false; + } if (block.isShadow()) { Blockly.navigation.warn_('Cannot move a shadow block to the workspace.'); return false; @@ -310,7 +341,7 @@ Blockly.navigation.moveBlockToWorkspace_ = function(block, wsNode) { * @private */ Blockly.navigation.modify_ = function() { - var markerNode = Blockly.getMainWorkspace().getMarker().getCurNode(); + var markerNode = Blockly.navigation.getMarker().getCurNode(); var cursorNode = Blockly.getMainWorkspace().getCursor().getCurNode(); if (!Blockly.navigation.modifyWarn_()) { return false; @@ -323,13 +354,17 @@ Blockly.navigation.modify_ = function() { var markerLoc = markerNode.getLocation(); if (markerNode.isConnection() && cursorNode.isConnection()) { + cursorLoc = /** @type {!Blockly.Connection} */ (cursorLoc); + markerLoc = /** @type {!Blockly.Connection} */ (markerLoc); return Blockly.navigation.connect_(cursorLoc, markerLoc); } else if (markerNode.isConnection() && - (cursorType == Blockly.ASTNode.types.BLOCK || - cursorType == Blockly.ASTNode.types.STACK)) { + (cursorType == Blockly.ASTNode.types.BLOCK || + cursorType == Blockly.ASTNode.types.STACK)) { + cursorLoc = /** @type {!Blockly.Block} */ (cursorLoc); + markerLoc = /** @type {!Blockly.Connection} */ (markerLoc); return Blockly.navigation.insertBlock(cursorLoc, markerLoc); } else if (markerType == Blockly.ASTNode.types.WORKSPACE) { - var block = Blockly.navigation.getSourceBlock_(cursorNode); + var block = cursorNode ? cursorNode.getSourceBlock() : null; return Blockly.navigation.moveBlockToWorkspace_(block, markerNode); } Blockly.navigation.warn_('Unexpected state in Blockly.navigation.modify_.'); @@ -367,12 +402,12 @@ Blockly.navigation.disconnectChild_ = function(movingConnection, destConnection) * @private */ Blockly.navigation.moveAndConnect_ = function(movingConnection, destConnection) { - if (!movingConnection || ! destConnection) { + if (!movingConnection || !destConnection) { return false; } var movingBlock = movingConnection.getSourceBlock(); - if (destConnection.canConnectWithReason_(movingConnection) == + if (destConnection.canConnectWithReason(movingConnection) == Blockly.Connection.CAN_CONNECT) { Blockly.navigation.disconnectChild_(movingConnection, destConnection); @@ -458,7 +493,7 @@ Blockly.navigation.connect_ = function(movingConnection, destConnection) { return true; } else { try { - destConnection.checkConnection_(movingConnection); + destConnection.checkConnection(movingConnection); } catch (e) { // If nothing worked report the error from the original connections. @@ -472,7 +507,7 @@ 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.Connection} destConnection The connection to connect to. * @return {boolean} Whether the connection was successful. */ Blockly.navigation.insertBlock = function(block, destConnection) { @@ -525,7 +560,7 @@ Blockly.navigation.disconnectBlocks_ = function() { Blockly.navigation.log_('Cannot disconnect blocks when the cursor is not on a connection'); return; } - var curConnection = curNode.getLocation(); + var curConnection = /** @type {!Blockly.Connection} */ (curNode.getLocation()); if (!curConnection.isConnected()) { Blockly.navigation.log_('Cannot disconnect unconnected connection'); return; @@ -541,7 +576,7 @@ Blockly.navigation.disconnectBlocks_ = function() { return; } superiorConnection.disconnect(); - inferiorConnection.bumpAwayFrom_(superiorConnection); + inferiorConnection.bumpAwayFrom(superiorConnection); var rootBlock = superiorConnection.getSourceBlock().getRootBlock(); rootBlock.bringToFront(); @@ -559,8 +594,8 @@ Blockly.navigation.disconnectBlocks_ = function() { * @private */ Blockly.navigation.markAtCursor_ = function() { - var workspace = Blockly.getMainWorkspace(); - workspace.getMarker().setCurNode(workspace.getCursor().getCurNode()); + Blockly.navigation.getMarker().setCurNode( + Blockly.getMainWorkspace().getCursor().getCurNode()); }; /** @@ -568,9 +603,9 @@ Blockly.navigation.markAtCursor_ = function() { * @private */ Blockly.navigation.removeMark_ = function() { - var workspace = Blockly.getMainWorkspace(); - workspace.getMarker().setCurNode(null); - workspace.getMarker().hide(); + var marker = Blockly.navigation.getMarker(); + marker.setCurNode(null); + marker.hide(); }; /** @@ -582,28 +617,6 @@ Blockly.navigation.setState = function(newState) { Blockly.navigation.currentState_ = newState; }; -/** - * Finds the source block of the location on a given node. - * @param {Blockly.ASTNode} node The node to find the source block on. - * @return {Blockly.Block} The source block of the location on the given node, - * or null if the node is of type workspace. - * @private - */ -Blockly.navigation.getSourceBlock_ = function(node) { - if (!node) { - return null; - } - if (node.getType() === Blockly.ASTNode.types.BLOCK) { - return /** @type {Blockly.Block} */ (node.getLocation()); - } else if (node.getType() === Blockly.ASTNode.types.STACK) { - return /** @type {Blockly.Block} */ (node.getLocation()); - } else if (node.getType() === Blockly.ASTNode.types.WORKSPACE) { - return null; - } else { - return node.getLocation().getSourceBlock(); - } -}; - /** * Gets the top node on a block. * This is either the previous connection, output connection or the block. @@ -635,7 +648,7 @@ Blockly.navigation.moveCursorOnBlockDelete = function(deletedBlock) { var cursor = workspace.getCursor(); if (cursor) { var curNode = cursor.getCurNode(); - var block = Blockly.navigation.getSourceBlock_(curNode); + var block = curNode ? curNode.getSourceBlock() : null; if (block === deletedBlock) { // If the block has a parent move the cursor to their connection point. @@ -652,7 +665,7 @@ Blockly.navigation.moveCursorOnBlockDelete = function(deletedBlock) { } // If the cursor is on a block whose parent is being deleted, move the // cursor to the workspace. - } else if (deletedBlock.getChildren(false).indexOf(block) > -1) { + } else if (block && deletedBlock.getChildren(false).indexOf(block) > -1) { cursor.setCurNode(Blockly.ASTNode.createWorkspaceNode(block.workspace, block.getRelativeToSurfaceXY())); } @@ -669,7 +682,7 @@ Blockly.navigation.moveCursorOnBlockMutation = function(mutatedBlock) { var cursor = Blockly.getMainWorkspace().getCursor(); if (cursor) { var curNode = cursor.getCurNode(); - var block = Blockly.navigation.getSourceBlock_(curNode); + var block = curNode ? curNode.getSourceBlock() : null; if (block === mutatedBlock) { cursor.setCurNode(Blockly.ASTNode.createBlockNode(block)); @@ -681,8 +694,8 @@ Blockly.navigation.moveCursorOnBlockMutation = function(mutatedBlock) { * Enable accessibility mode. */ Blockly.navigation.enableKeyboardAccessibility = function() { - if (!Blockly.keyboardAccessibilityMode) { - Blockly.keyboardAccessibilityMode = true; + if (!Blockly.getMainWorkspace().keyboardAccessibilityMode) { + Blockly.getMainWorkspace().keyboardAccessibilityMode = true; Blockly.navigation.focusWorkspace_(); } }; @@ -691,11 +704,11 @@ Blockly.navigation.enableKeyboardAccessibility = function() { * Disable accessibility mode. */ Blockly.navigation.disableKeyboardAccessibility = function() { - if (Blockly.keyboardAccessibilityMode) { + if (Blockly.getMainWorkspace().keyboardAccessibilityMode) { var workspace = Blockly.getMainWorkspace(); - Blockly.keyboardAccessibilityMode = false; + Blockly.getMainWorkspace().keyboardAccessibilityMode = false; workspace.getCursor().hide(); - workspace.getMarker().hide(); + Blockly.navigation.getMarker().hide(); if (Blockly.navigation.getFlyoutCursor_()) { Blockly.navigation.getFlyoutCursor_().hide(); } @@ -764,8 +777,8 @@ Blockly.navigation.onKeyPress = function(e) { }; /** - * Execute any actions on the flyout, workspace, or toolbox that correspond to - * the given action. + * Decides which actions to handle depending on keyboard navigation and readonly + * states. * @param {!Blockly.Action} action The current action. * @return {boolean} True if the action has been handled, false otherwise. */ @@ -773,7 +786,7 @@ Blockly.navigation.onBlocklyAction = function(action) { var readOnly = Blockly.getMainWorkspace().options.readOnly; var actionHandled = false; - if (Blockly.keyboardAccessibilityMode) { + if (Blockly.getMainWorkspace().keyboardAccessibilityMode) { if (!readOnly) { actionHandled = Blockly.navigation.handleActions_(action); // If in readonly mode only handle valid actions. @@ -790,92 +803,44 @@ Blockly.navigation.onBlocklyAction = function(action) { /** * Handles the action or dispatches to the appropriate action handler. - * @param {!Blockly.Action} action The current action + * @param {!Blockly.Action} action The action to handle. * @return {boolean} True if the action has been handled, false otherwise. * @private */ Blockly.navigation.handleActions_ = function(action) { - var workspace = Blockly.getMainWorkspace(); - if (action.name === Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV) { + if (action.name == Blockly.navigation.actionNames.TOOLBOX || + Blockly.navigation.currentState_ == Blockly.navigation.STATE_TOOLBOX) { + return Blockly.navigation.toolboxOnAction_(action); + } else if (action.name == Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV) { Blockly.navigation.disableKeyboardAccessibility(); return true; - } else if (action.name === Blockly.navigation.actionNames.TOOLBOX) { - if (!workspace.getToolbox()) { - Blockly.navigation.focusFlyout_(); - } else { - Blockly.navigation.focusToolbox_(); - } - return true; - } else if (Blockly.navigation.currentState_ === Blockly.navigation.STATE_WS) { - var curNode = workspace.getCursor().getCurNode(); - var actionHandled = false; - if (curNode && curNode.getType() === Blockly.ASTNode.types.FIELD) { - actionHandled = curNode.getLocation().onBlocklyAction(action); - } - if (!actionHandled) { - actionHandled = Blockly.navigation.workspaceOnAction_(action); - } - return actionHandled; - } else if (Blockly.navigation.currentState_ === Blockly.navigation.STATE_FLYOUT) { + } if (Blockly.navigation.currentState_ == Blockly.navigation.STATE_WS) { + return Blockly.navigation.workspaceOnAction_(action); + } else if (Blockly.navigation.currentState_ == Blockly.navigation.STATE_FLYOUT) { return Blockly.navigation.flyoutOnAction_(action); - } else if (Blockly.navigation.currentState_ === Blockly.navigation.STATE_TOOLBOX) { - return Blockly.navigation.toolboxOnAction_(action); } return false; }; /** - * Handle all actions performed on the workspace. - * @param {!Blockly.Action} action The action to handle. - * @return {boolean} True if the action has been handled, false otherwise. - * @private - */ -Blockly.navigation.workspaceOnAction_ = function(action) { - var workspace = Blockly.getMainWorkspace(); - switch (action.name) { - case Blockly.navigation.actionNames.PREVIOUS: - workspace.getCursor().prev(); - return true; - case Blockly.navigation.actionNames.OUT: - workspace.getCursor().out(); - return true; - case Blockly.navigation.actionNames.NEXT: - workspace.getCursor().next(); - return true; - case Blockly.navigation.actionNames.IN: - workspace.getCursor().in(); - return true; - case Blockly.navigation.actionNames.INSERT: - Blockly.navigation.modify_(); - return true; - case Blockly.navigation.actionNames.MARK: - Blockly.navigation.handleEnterForWS_(); - return true; - case Blockly.navigation.actionNames.DISCONNECT: - Blockly.navigation.disconnectBlocks_(); - return true; - default: - return false; - } -}; - -/** - * Handle all actions performed on the flyout. + * Handles the given action for the flyout. * @param {!Blockly.Action} action The action to handle. * @return {boolean} True if the action has been handled, false otherwise. * @private */ Blockly.navigation.flyoutOnAction_ = function(action) { + var workspace = Blockly.getMainWorkspace(); + var toolbox = workspace.getToolbox(); + var flyout = toolbox ? toolbox.flyout_ : workspace.getFlyout(); + + if (flyout && flyout.onBlocklyAction(action)) { + return true; + } + switch (action.name) { - case Blockly.navigation.actionNames.PREVIOUS: - Blockly.navigation.getFlyoutCursor_().prev(); - return true; case Blockly.navigation.actionNames.OUT: Blockly.navigation.focusToolbox_(); return true; - case Blockly.navigation.actionNames.NEXT: - Blockly.navigation.getFlyoutCursor_().next(); - return true; case Blockly.navigation.actionNames.MARK: Blockly.navigation.insertFromFlyout(); return true; @@ -888,24 +853,92 @@ Blockly.navigation.flyoutOnAction_ = function(action) { }; /** - * Handle all actions performeed on the toolbox. + * Handles the given action for the toolbox. * @param {!Blockly.Action} action The action to handle. * @return {boolean} True if the action has been handled, false otherwise. * @private */ Blockly.navigation.toolboxOnAction_ = function(action) { - if (action.name === Blockly.navigation.actionNames.EXIT) { - Blockly.navigation.focusWorkspace_(); - return true; - } - var toolbox = Blockly.getMainWorkspace().getToolbox(); - var handled = toolbox.onBlocklyAction(action); - if (!handled && action.name === Blockly.navigation.actionNames.IN) { - Blockly.navigation.focusFlyout_(); + var workspace = Blockly.getMainWorkspace(); + var toolbox = workspace.getToolbox(); + var handled = toolbox ? toolbox.onBlocklyAction(action) : false; + + if (handled) { return true; } - return handled; + if (action.name === Blockly.navigation.actionNames.TOOLBOX) { + if (!workspace.getToolbox()) { + Blockly.navigation.focusFlyout_(); + } else { + Blockly.navigation.focusToolbox_(); + } + return true; + } else if (action.name === Blockly.navigation.actionNames.IN) { + Blockly.navigation.focusFlyout_(); + return true; + } else if (action.name === Blockly.navigation.actionNames.EXIT) { + Blockly.navigation.focusWorkspace_(); + return true; + } + return false; +}; + +/** + * Move the workspace cursor in the given direction. + * @param {number} xDirection -1 to move cursor left. 1 to move cursor right. + * @param {number} yDirection -1 to move cursor up. 1 to move cursor down. + * @return {boolean} True if the current node is a workspace, false otherwise. + * @private + */ +Blockly.navigation.moveWSCursor_ = function(xDirection, yDirection) { + var cursor = Blockly.getMainWorkspace().getCursor(); + var curNode = Blockly.getMainWorkspace().getCursor().getCurNode(); + + if (curNode.getType() !== Blockly.ASTNode.types.WORKSPACE) { + return false; + } + + var wsCoord = curNode.getWsCoordinate(); + var newX = xDirection * Blockly.navigation.WS_MOVE_DISTANCE + wsCoord.x; + var newY = yDirection * Blockly.navigation.WS_MOVE_DISTANCE + wsCoord.y; + + cursor.setCurNode(Blockly.ASTNode.createWorkspaceNode( + Blockly.getMainWorkspace(), new Blockly.utils.Coordinate(newX, newY))); + return true; +}; + +/** + * Handles the given action for the workspace. + * @param {!Blockly.Action} action The action to handle. + * @return {boolean} True if the action has been handled, false otherwise. + * @private + */ +Blockly.navigation.workspaceOnAction_ = function(action) { + if (Blockly.getMainWorkspace().getCursor().onBlocklyAction(action)) { + return true; + } + switch (action.name) { + case Blockly.navigation.actionNames.INSERT: + Blockly.navigation.modify_(); + return true; + case Blockly.navigation.actionNames.MARK: + Blockly.navigation.handleEnterForWS_(); + return true; + case Blockly.navigation.actionNames.DISCONNECT: + Blockly.navigation.disconnectBlocks_(); + return true; + case Blockly.navigation.actionNames.MOVE_WS_CURSOR_UP: + return Blockly.navigation.moveWSCursor_(0, -1); + case Blockly.navigation.actionNames.MOVE_WS_CURSOR_DOWN: + return Blockly.navigation.moveWSCursor_(0, 1); + case Blockly.navigation.actionNames.MOVE_WS_CURSOR_LEFT: + return Blockly.navigation.moveWSCursor_(-1, 0); + case Blockly.navigation.actionNames.MOVE_WS_CURSOR_RIGHT: + return Blockly.navigation.moveWSCursor_(1, 0); + default: + return false; + } }; /** @@ -917,7 +950,7 @@ Blockly.navigation.handleEnterForWS_ = function() { var curNode = cursor.getCurNode(); var nodeType = curNode.getType(); if (nodeType == Blockly.ASTNode.types.FIELD) { - curNode.getLocation().showEditor_(); + curNode.getLocation().showEditor(); } else if (curNode.isConnection() || nodeType == Blockly.ASTNode.types.WORKSPACE) { Blockly.navigation.markAtCursor_(); @@ -944,7 +977,8 @@ Blockly.navigation.ACTION_PREVIOUS = new Blockly.Action( * @type {!Blockly.Action} */ Blockly.navigation.ACTION_OUT = new Blockly.Action( - Blockly.navigation.actionNames.OUT, 'Go to the parent of the current location.'); + Blockly.navigation.actionNames.OUT, + 'Go to the parent of the current location.'); /** * The next action. @@ -958,7 +992,8 @@ Blockly.navigation.ACTION_NEXT = new Blockly.Action( * @type {!Blockly.Action} */ Blockly.navigation.ACTION_IN = new Blockly.Action( - Blockly.navigation.actionNames.IN, 'Go to the first child of the current location.'); + Blockly.navigation.actionNames.IN, + 'Go to the first child of the current location.'); /** * The action to try to insert a block. @@ -980,8 +1015,8 @@ Blockly.navigation.ACTION_MARK = new Blockly.Action( * @type {!Blockly.Action} */ Blockly.navigation.ACTION_DISCONNECT = new Blockly.Action( - Blockly.navigation.actionNames.DISCONNECT, 'Dicsonnect the block at the' + - 'current location from its parent.'); + Blockly.navigation.actionNames.DISCONNECT, + 'Disconnect the block at the current location from its parent.'); /** * The action to open the toolbox. @@ -995,14 +1030,49 @@ Blockly.navigation.ACTION_TOOLBOX = new Blockly.Action( * @type {!Blockly.Action} */ Blockly.navigation.ACTION_EXIT = new Blockly.Action( - Blockly.navigation.actionNames.EXIT, 'Close the current modal, such as a toolbox or field editor.'); + Blockly.navigation.actionNames.EXIT, + 'Close the current modal, such as a toolbox or field editor.'); /** * The action to toggle keyboard navigation mode on and off. * @type {!Blockly.Action} */ Blockly.navigation.ACTION_TOGGLE_KEYBOARD_NAV = new Blockly.Action( - Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV, 'Turns on and off keyboard navigation.'); + Blockly.navigation.actionNames.TOGGLE_KEYBOARD_NAV, + 'Turns on and off keyboard navigation.'); + +/** + * The action to move the cursor to the keft on a worksapce. + * @type {!Blockly.Action} + */ +Blockly.navigation.ACTION_MOVE_WS_CURSOR_LEFT = new Blockly.Action( + Blockly.navigation.actionNames.MOVE_WS_CURSOR_LEFT, + 'Move the workspace cursor to the lefts.'); + +/** + * The action to move the cursor to the right on a worksapce. + * @type {!Blockly.Action} + */ +Blockly.navigation.ACTION_MOVE_WS_CURSOR_RIGHT = new Blockly.Action( + Blockly.navigation.actionNames.MOVE_WS_CURSOR_RIGHT, + 'Move the workspace cursor to the right.'); + +/** + * The action to move the cursor up on a worksapce. + * @type {!Blockly.Action} + */ +Blockly.navigation.ACTION_MOVE_WS_CURSOR_UP = new Blockly.Action( + Blockly.navigation.actionNames.MOVE_WS_CURSOR_UP, + 'Move the workspace cursor up.'); + +/** + * The action to move the cursor down on a worksapce. + * @type {!Blockly.Action} + */ +Blockly.navigation.ACTION_MOVE_WS_CURSOR_DOWN = new Blockly.Action( + Blockly.navigation.actionNames.MOVE_WS_CURSOR_DOWN, + 'Move the workspace cursor down.'); + /** * List of actions that can be performed in read only mode. diff --git a/core/keyboard_nav/tab_navigate_cursor.js b/core/keyboard_nav/tab_navigate_cursor.js new file mode 100644 index 000000000..4074f3588 --- /dev/null +++ b/core/keyboard_nav/tab_navigate_cursor.js @@ -0,0 +1,60 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The class representing a cursor that is used to navigate + * between tab navigable fields. + * @author samelh@google.com (Sam El-Husseini) + */ +'use strict'; + +goog.provide('Blockly.TabNavigateCursor'); + +goog.require('Blockly.ASTNode'); +goog.require('Blockly.BasicCursor'); +goog.require('Blockly.utils.object'); + + +/** + * A cursor for navigating between tab navigable fields. + * @constructor + * @extends {Blockly.BasicCursor} + */ +Blockly.TabNavigateCursor = function() { + Blockly.TabNavigateCursor.superClass_.constructor.call(this); +}; +Blockly.utils.object.inherits(Blockly.TabNavigateCursor, Blockly.BasicCursor); + +/** + * Skip all nodes except for tab navigable fields. + * @param {Blockly.ASTNode} node The AST node to check whether it is valid. + * @return {boolean} True if the node should be visited, false otherwise. + * @override + */ +Blockly.TabNavigateCursor.prototype.validNode_ = function(node) { + var isValid = false; + var type = node && node.getType(); + if (node) { + var location = node.getLocation(); + if (type == Blockly.ASTNode.types.FIELD && + location && location.isTabNavigable() && + (/** @type {!Blockly.Field} */ (location)).isClickable()) { + isValid = true; + } + } + return isValid; +}; diff --git a/core/marker_manager.js b/core/marker_manager.js new file mode 100644 index 000000000..e64ff2d74 --- /dev/null +++ b/core/marker_manager.js @@ -0,0 +1,182 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Object in charge of managing markers and the cursor. + * @author aschmiedt@google.com (Abby Schmiedt) + */ +'use strict'; + +goog.provide('Blockly.MarkerManager'); + +goog.require('Blockly.Cursor'); +goog.require('Blockly.Marker'); + + +/** + * Class to manage the multiple markers and the cursor on a workspace. + * @param {!Blockly.WorkspaceSvg} workspace The workspace for the marker manager. + * @constructor + * @package + */ +Blockly.MarkerManager = function(workspace){ + /** + * The cursor. + * @type {Blockly.Cursor} + * @private + */ + this.cursor_ = null; + + /** + * The cursor's svg element. + * @type {SVGElement} + * @private + */ + this.cursorSvg_ = null; + + /** + * The map of markers for the workspace. + * @type {!Object} + * @private + */ + this.markers_ = {}; + + /** + * The workspace this marker manager is associated with. + * @type {!Blockly.WorkspaceSvg} + * @private + */ + this.workspace_ = workspace; +}; + +/** + * Register the marker by adding it to the map of markers. + * @param {string} id A unique identifier for the marker. + * @param {!Blockly.Marker} marker The marker to register. + */ +Blockly.MarkerManager.prototype.registerMarker = function(id, marker) { + if (this.markers_[id]) { + this.unregisterMarker(id); + } + marker.setDrawer(this.workspace_.getRenderer() + .makeMarkerDrawer(this.workspace_, marker)); + this.setMarkerSvg(marker.getDrawer().createDom()); + this.markers_[id] = marker; +}; + +/** + * Unregister the marker by removing it from the map of markers. + * @param {string} id The id of the marker to unregister. + */ +Blockly.MarkerManager.prototype.unregisterMarker = function(id) { + var marker = this.markers_[id]; + if (marker) { + marker.dispose(); + delete this.markers_[id]; + } else { + throw Error('Marker with id ' + id + ' does not exist. Can only unregister' + + 'markers that exist.'); + } +}; + +/** + * Get the cursor for the workspace. + * @return {Blockly.Cursor} The cursor for this workspace. + */ +Blockly.MarkerManager.prototype.getCursor = function() { + return this.cursor_; +}; + +/** + * Get a single marker that corresponds to the given id. + * @param {string} id A unique identifier for the marker. + * @return {Blockly.Marker} The marker that corresponds to the given id, or null + * if none exists. + */ +Blockly.MarkerManager.prototype.getMarker = function(id) { + return this.markers_[id]; +}; + +/** + * Sets the cursor and initializes the drawer for use with keyboard navigation. + * @param {Blockly.Cursor} cursor The cursor used to move around this workspace. + */ +Blockly.MarkerManager.prototype.setCursor = function(cursor) { + if (this.cursor_ && this.cursor_.getDrawer()) { + this.cursor_.getDrawer().dispose(); + } + this.cursor_ = cursor; + if (this.cursor_) { + var drawer = this.workspace_.getRenderer() + .makeMarkerDrawer(this.workspace_, this.cursor_); + this.cursor_.setDrawer(drawer); + this.setCursorSvg(this.cursor_.getDrawer().createDom()); + } +}; + +/** + * Add the cursor svg to this workspace svg group. + * @param {SVGElement} cursorSvg The svg root of the cursor to be added to the + * workspace svg group. + * @package + */ +Blockly.MarkerManager.prototype.setCursorSvg = function(cursorSvg) { + if (!cursorSvg) { + this.cursorSvg_ = null; + return; + } + + this.workspace_.getBlockCanvas().appendChild(cursorSvg); + this.cursorSvg_ = cursorSvg; +}; + +/** + * Add the marker svg to this workspaces svg group. + * @param {SVGElement} markerSvg The svg root of the marker to be added to the + * workspace svg group. + * @package + */ +Blockly.MarkerManager.prototype.setMarkerSvg = function(markerSvg) { + if (!markerSvg) { + this.markerSvg_ = null; + return; + } + + if (this.workspace_.getBlockCanvas()) { + if (this.cursorSvg_) { + this.workspace_.getBlockCanvas().insertBefore(markerSvg, this.cursorSvg_); + } else { + this.workspace_.getBlockCanvas().appendChild(markerSvg); + } + } +}; + +/** + * Dispose of the marker manager. + * Go through and delete all markers associated with this marker manager. + * @suppress {checkTypes} + * @package + */ +Blockly.MarkerManager.prototype.dispose = function() { + var markerIds = Object.keys(this.markers_); + for (var i = 0, markerId; (markerId = markerIds[i]); i++) { + this.unregisterMarker(markerId); + } + this.markers_ = null; + this.cursor_.dispose(); + this.cursor_ = null; +}; diff --git a/core/mutator.js b/core/mutator.js index b459cab58..9971f26be 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -29,6 +29,7 @@ goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockChange'); goog.require('Blockly.Events.Ui'); goog.require('Blockly.Icon'); +goog.require('Blockly.navigation'); goog.require('Blockly.utils'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.global'); @@ -62,6 +63,25 @@ Blockly.Mutator.prototype.workspaceWidth_ = 0; */ Blockly.Mutator.prototype.workspaceHeight_ = 0; +/** + * Set the block this mutator is associated with. + * @param {Blockly.BlockSvg} block The block associated with this mutator. + * @package + */ +Blockly.Mutator.prototype.setBlock = function(block) { + this.block_ = block; +}; + +/** + * Returns the workspace inside this mutator icon's bubble. + * @return {Blockly.WorkspaceSvg} The workspace inside this mutator icon's + * bubble. + * @package + */ +Blockly.Mutator.prototype.getWorkspace = function() { + return this.workspace_; +}; + /** * Draw the mutator icon. * @param {!Element} group The icon group. @@ -133,7 +153,7 @@ Blockly.Mutator.prototype.createEditor_ = function() { // Convert the list of names into a list of XML objects for the flyout. if (this.quarkNames_.length) { var quarkXml = Blockly.utils.xml.createElement('xml'); - for (var i = 0, quarkName; quarkName = this.quarkNames_[i]; i++) { + for (var i = 0, quarkName; (quarkName = this.quarkNames_[i]); i++) { var element = Blockly.utils.xml.createElement('block'); element.setAttribute('type', quarkName); quarkXml.appendChild(element); @@ -141,22 +161,22 @@ Blockly.Mutator.prototype.createEditor_ = function() { } else { var quarkXml = null; } - var workspaceOptions = { - // If you want to enable disabling, also remove the - // event filter from workspaceChanged_ . - disable: false, - disabledPatternId: this.block_.workspace.options.disabledPatternId, - languageTree: quarkXml, - parentWorkspace: this.block_.workspace, - pathToMedia: this.block_.workspace.options.pathToMedia, - RTL: this.block_.RTL, - toolboxPosition: this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT : - Blockly.TOOLBOX_AT_LEFT, - horizontalLayout: false, - getMetrics: this.getFlyoutMetrics_.bind(this), - setMetrics: null, - renderer: this.block_.workspace.options.renderer - }; + var workspaceOptions = new Blockly.Options( + /** @type {!Blockly.BlocklyOptions} */ + ({ + // If you want to enable disabling, also remove the + // event filter from workspaceChanged_ . + 'disable': false, + 'parentWorkspace': this.block_.workspace, + 'media': this.block_.workspace.options.pathToMedia, + 'rtl': this.block_.RTL, + 'horizontalLayout': false, + 'renderer': this.block_.workspace.options.renderer + })); + workspaceOptions.toolboxPosition = this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT : + Blockly.TOOLBOX_AT_LEFT; + workspaceOptions.languageTree = quarkXml; + workspaceOptions.getMetrics = this.getFlyoutMetrics_.bind(this); this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions); this.workspace_.isMutator = true; this.workspace_.addChangeListener(Blockly.Events.disableOrphans); @@ -165,7 +185,7 @@ 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 = this.workspace_.addFlyout('g'); var background = this.workspace_.createDom('blocklyMutatorBackground'); // Insert the flyout after the but before the block canvas so that @@ -215,8 +235,9 @@ Blockly.Mutator.prototype.resizeBubble_ = function() { width = workspaceSize.width + workspaceSize.x; } var height = workspaceSize.height + doubleBorderWidth * 3; - if (this.workspace_.flyout_) { - var flyoutMetrics = this.workspace_.flyout_.getMetrics_(); + var flyout = this.workspace_.getFlyout(); + if (flyout) { + var flyoutMetrics = flyout.getMetrics_(); height = Math.max(height, flyoutMetrics.contentHeight + 20); } width += doubleBorderWidth * 3; @@ -241,6 +262,16 @@ Blockly.Mutator.prototype.resizeBubble_ = function() { this.workspace_.resize(); }; +/** + * A method handler for when the bubble is moved. + * @private + */ +Blockly.Mutator.prototype.onBubbleMove_ = function() { + if (this.workspace_) { + this.workspace_.recordDeleteAreas(); + } +}; + /** * Show or hide the mutator bubble. * @param {boolean} visible True if the bubble should be visible. @@ -256,26 +287,29 @@ Blockly.Mutator.prototype.setVisible = function(visible) { // Create the bubble. this.bubble_ = new Blockly.Bubble( /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), - this.createEditor_(), this.block_.svgPath_, this.iconXY_, null, null); + this.createEditor_(), this.block_.pathObject.svgPath, + /** @type {!Blockly.utils.Coordinate} */ (this.iconXY_), null, null); // Expose this mutator's block's ID on its top-level SVG group. this.bubble_.setSvgId(this.block_.id); + this.bubble_.registerMoveEvent(this.onBubbleMove_.bind(this)); var tree = this.workspace_.options.languageTree; + var flyout = this.workspace_.getFlyout(); if (tree) { - this.workspace_.flyout_.init(this.workspace_); - this.workspace_.flyout_.show(tree.childNodes); + flyout.init(this.workspace_); + flyout.show(tree.childNodes); } this.rootBlock_ = this.block_.decompose(this.workspace_); var blocks = this.rootBlock_.getDescendants(false); - for (var i = 0, child; child = blocks[i]; i++) { + for (var i = 0, child; (child = blocks[i]); i++) { child.render(); } // The root block should not be dragable or deletable. this.rootBlock_.setMovable(false); this.rootBlock_.setDeletable(false); - if (this.workspace_.flyout_) { - var margin = this.workspace_.flyout_.CORNER_RADIUS * 2; - var x = this.workspace_.getFlyout().getWidth() + margin; + if (flyout) { + var margin = flyout.CORNER_RADIUS * 2; + var x = flyout.getWidth() + margin; } else { var margin = 16; var x = margin; @@ -287,16 +321,19 @@ Blockly.Mutator.prototype.setVisible = function(visible) { // Save the initial connections, then listen for further changes. if (this.block_.saveConnections) { var thisMutator = this; - this.block_.saveConnections(this.rootBlock_); + var mutatorBlock = + /** @type {{saveConnections: function(!Blockly.Block)}} */ ( + this.block_); + mutatorBlock.saveConnections(this.rootBlock_); this.sourceListener_ = function() { - thisMutator.block_.saveConnections(thisMutator.rootBlock_); + mutatorBlock.saveConnections(thisMutator.rootBlock_); }; this.block_.workspace.addChangeListener(this.sourceListener_); } this.resizeBubble_(); // When the mutator's workspace changes, update the source block. this.workspace_.addChangeListener(this.workspaceChanged_.bind(this)); - this.updateColour(); + this.applyColour(); } else { // Dispose of the bubble. this.svgDialog_ = null; @@ -330,7 +367,7 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { if (!this.workspace_.isDragging()) { var blocks = this.workspace_.getTopBlocks(false); var MARGIN = 20; - for (var b = 0, block; block = blocks[b]; b++) { + for (var b = 0, block; (block = blocks[b]); b++) { var blockXY = block.getRelativeToSurfaceXY(); var blockHW = block.getHeightWidth(); if (blockXY.y + blockHW.height < MARGIN) { @@ -346,15 +383,14 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { var block = this.block_; var oldMutationDom = block.mutationToDom(); var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); - // Switch off rendering while the source block is rebuilt. - var savedRendered = block.rendered; - block.rendered = false; // Allow the source block to rebuild itself. block.compose(this.rootBlock_); - // Restore rendering and show the changes. - block.rendered = savedRendered; - // Mutation may have added some elements that need initializing. block.initSvg(); + block.render(); + + if (Blockly.getMainWorkspace().keyboardAccessibilityMode) { + Blockly.navigation.moveCursorOnBlockMutation(block); + } var newMutationDom = block.mutationToDom(); var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); if (oldMutation != newMutation) { @@ -368,13 +404,7 @@ Blockly.Mutator.prototype.workspaceChanged_ = function(e) { Blockly.Events.setGroup(false); }, Blockly.BUMP_DELAY); } - if (block.rendered) { - block.render(); - } - if (oldMutation != newMutation && Blockly.keyboardAccessibilityMode) { - Blockly.navigation.moveCursorOnBlockMutation(block); - } // Don't update the bubble until the drag has ended, to avoid moving blocks // under the cursor. if (!this.workspace_.isDragging()) { @@ -420,14 +450,14 @@ Blockly.Mutator.prototype.dispose = function() { Blockly.Mutator.prototype.updateBlockStyle = function() { var ws = this.workspace_; - if (ws && ws.getAllBlocks()) { - var workspaceBlocks = ws.getAllBlocks(); + if (ws && ws.getAllBlocks(false)) { + var workspaceBlocks = ws.getAllBlocks(false); for (var i = 0; i < workspaceBlocks.length; i++) { var block = workspaceBlocks[i]; block.setStyle(block.getStyleName()); } - var flyoutBlocks = ws.flyout_.workspace_.getAllBlocks(); + var flyoutBlocks = ws.getFlyout().workspace_.getAllBlocks(false); for (var i = 0; i < flyoutBlocks.length; i++) { var block = flyoutBlocks[i]; block.setStyle(block.getStyleName()); diff --git a/core/names.js b/core/names.js index 263ef81ef..daa7edb73 100644 --- a/core/names.js +++ b/core/names.js @@ -76,7 +76,6 @@ Blockly.Names.prototype.reset = function() { /** * Set the variable map that maps from variable name to variable object. * @param {!Blockly.VariableMap} map The map to track. - * @package */ Blockly.Names.prototype.setVariableMap = function(map) { this.variableMap_ = map; @@ -84,7 +83,8 @@ Blockly.Names.prototype.setVariableMap = function(map) { /** * Get the name for a user-defined variable, based on its ID. - * This should only be used for variables of type Blockly.Variables.NAME_TYPE. + * This should only be used for variables of type + * Blockly.VARIABLE_CATEGORY_NAME. * @param {string} id The ID to look up in the variable map. * @return {?string} The name of the referenced variable, or null if there was * no variable map or the variable was not found in the map. @@ -113,10 +113,9 @@ Blockly.Names.prototype.getNameForUserVariable_ = function(id) { * @param {string} type The type of entity in Blockly * ('VARIABLE', 'PROCEDURE', 'BUILTIN', etc...). * @return {string} An entity name that is legal in the exported language. - * @suppress {deprecated} Suppress deprecated Blockly.Variables.NAME_TYPE. */ Blockly.Names.prototype.getName = function(name, type) { - if (type == Blockly.Variables.NAME_TYPE) { + if (type == Blockly.VARIABLE_CATEGORY_NAME) { var varName = this.getNameForUserVariable_(name); if (varName) { name = varName; @@ -124,7 +123,7 @@ Blockly.Names.prototype.getName = function(name, type) { } var normalized = name.toLowerCase() + '_' + type; - var isVarType = type == Blockly.Variables.NAME_TYPE || + var isVarType = type == Blockly.VARIABLE_CATEGORY_NAME || type == Blockly.Names.DEVELOPER_VARIABLE_TYPE; var prefix = isVarType ? this.variablePrefix_ : ''; @@ -156,7 +155,7 @@ Blockly.Names.prototype.getDistinctName = function(name, type) { } safeName += i; this.dbReverse_[safeName] = true; - var isVarType = type == Blockly.Variables.NAME_TYPE || + var isVarType = type == Blockly.VARIABLE_CATEGORY_NAME || type == Blockly.Names.DEVELOPER_VARIABLE_TYPE; var prefix = isVarType ? this.variablePrefix_ : ''; return prefix + safeName; diff --git a/core/options.js b/core/options.js index ed081da42..cf0d8dba2 100644 --- a/core/options.js +++ b/core/options.js @@ -23,6 +23,9 @@ goog.provide('Blockly.Options'); +goog.require('Blockly.Theme'); +goog.require('Blockly.Themes.Classic'); +goog.require('Blockly.user.keyMap'); goog.require('Blockly.utils.userAgent'); goog.require('Blockly.Xml'); @@ -30,8 +33,8 @@ goog.require('Blockly.Xml'); /** * Parse the user-specified options, using reasonable defaults where behaviour * is unspecified. - * @param {!Object} options Dictionary of options. Specification: - * https://developers.google.com/blockly/guides/get-started/web#configuration + * @param {!Blockly.BlocklyOptions} options Dictionary of options. + * Specification: https://developers.google.com/blockly/guides/get-started/web#configuration * @constructor */ Blockly.Options = function(options) { @@ -84,11 +87,7 @@ Blockly.Options = function(options) { horizontalLayout = false; } var toolboxAtStart = options['toolboxPosition']; - if (toolboxAtStart === 'end') { - toolboxAtStart = false; - } else { - toolboxAtStart = true; - } + toolboxAtStart = toolboxAtStart !== 'end'; if (horizontalLayout) { var toolboxPosition = toolboxAtStart ? @@ -114,7 +113,6 @@ Blockly.Options = function(options) { } else { var oneBasedIndex = !!options['oneBasedIndex']; } - var theme = options['theme']; var keyMap = options['keyMap'] || Blockly.user.keyMap.createDefaultKeyMap(); var renderer = options['renderer'] || 'geras'; @@ -141,27 +139,45 @@ Blockly.Options = function(options) { this.gridOptions = Blockly.Options.parseGridOptions_(options); this.zoomOptions = Blockly.Options.parseZoomOptions_(options); this.toolboxPosition = toolboxPosition; - this.theme = theme; + this.theme = Blockly.Options.parseThemeOptions_(options); this.keyMap = keyMap; this.renderer = renderer; + + /** + * The SVG element for the grid pattern. + * Created during injection. + * @type {!SVGElement} + */ + this.gridPattern = undefined; + + /** + * The parent of the current workspace, or null if there is no parent + * workspace. + * @type {Blockly.Workspace} + */ + this.parentWorkspace = options['parentWorkspace']; }; /** - * The parent of the current workspace, or null if there is no parent workspace. - * @type {Blockly.Workspace} + * Blockly options. + * This interface is further described in `typings/blockly-interfaces.d.ts`. + * @interface */ -Blockly.Options.prototype.parentWorkspace = null; +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. + * @return {void} */ -Blockly.Options.prototype.setMetrics = null; +Blockly.Options.prototype.setMetrics; /** * Return an object with the metrics required to size the workspace. - * @return {Object} Contains size and position metrics, or null. + * @return {!Object} Contains size and position metrics. */ -Blockly.Options.prototype.getMetrics = null; +Blockly.Options.prototype.getMetrics; /** * Parse the user-specified move options, using reasonable defaults where @@ -237,6 +253,11 @@ Blockly.Options.parseZoomOptions_ = function(options) { } else { zoomOptions.scaleSpeed = Number(zoom['scaleSpeed']); } + if (zoom['pinch'] === undefined) { + zoomOptions.pinch = zoomOptions.wheel || zoomOptions.controls; + } else { + zoomOptions.pinch = !!zoom['pinch']; + } return zoomOptions; }; @@ -253,11 +274,28 @@ Blockly.Options.parseGridOptions_ = function(options) { var gridOptions = {}; gridOptions.spacing = Number(grid['spacing']) || 0; gridOptions.colour = grid['colour'] || '#888'; - gridOptions.length = Number(grid['length']) || 1; + gridOptions.length = + (grid['length'] === undefined) ? 1 : Number(grid['length']); gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap']; return gridOptions; }; +/** + * Parse the user-specified theme options, using the classic theme as a default. + * https://developers.google.com/blockly/guides/configure/web/themes + * @param {!Object} options Dictionary of options. + * @return {!Blockly.Theme} A Blockly Theme. + * @private + */ +Blockly.Options.parseThemeOptions_ = function(options) { + var theme = options['theme'] || Blockly.Themes.Classic; + if (theme instanceof Blockly.Theme) { + return /** @type {!Blockly.Theme} */ (theme); + } + return new Blockly.Theme('builtin', + theme['blockStyles'], theme['categoryStyles'], theme['componentStyles']); +}; + /** * Parse the provided toolbox tree into a consistent DOM format. * @param {Node|string} tree DOM tree of blocks, or text representation of same. diff --git a/core/procedures.js b/core/procedures.js index 5388bf943..b0bb55651 100644 --- a/core/procedures.js +++ b/core/procedures.js @@ -46,6 +46,22 @@ goog.require('Blockly.Xml'); */ Blockly.Procedures.NAME_TYPE = Blockly.PROCEDURE_CATEGORY_NAME; +/** + * The default argument for a procedures_mutatorarg block. + * @type {string} + */ +Blockly.Procedures.DEFAULT_ARG = 'x'; + +/** + * Procedure block type. + * @typedef {{ + * getProcedureCall: function():string, + * renameProcedure: function(string,string), + * getProcedureDef: function():!Array + * }} + */ +Blockly.Procedures.ProcedureBlock; + /** * Find all user-created procedure definitions in a workspace. * @param {!Blockly.Workspace} root Root workspace. @@ -60,7 +76,9 @@ Blockly.Procedures.allProcedures = function(root) { var proceduresNoReturn = []; for (var i = 0; i < blocks.length; i++) { if (blocks[i].getProcedureDef) { - var tuple = blocks[i].getProcedureDef(); + var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( + blocks[i]); + var tuple = procedureBlock.getProcedureDef(); if (tuple) { if (tuple[2]) { proceduresReturn.push(tuple); @@ -143,7 +161,9 @@ Blockly.Procedures.isNameUsed = function(name, workspace, opt_exclude) { continue; } if (blocks[i].getProcedureDef) { - var procName = blocks[i].getProcedureDef(); + var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( + blocks[i]); + var procName = procedureBlock.getProcedureDef(); if (Blockly.Names.equals(procName[0], name)) { return true; } @@ -162,14 +182,18 @@ Blockly.Procedures.rename = function(name) { // Strip leading and trailing whitespace. Beyond this, all names are legal. name = name.trim(); - var legalName = Blockly.Procedures.findLegalName(name, this.getSourceBlock()); + var legalName = Blockly.Procedures.findLegalName(name, + /** @type {!Blockly.Block} */ (this.getSourceBlock())); var oldName = this.getValue(); if (oldName != name && oldName != legalName) { // Rename any callers. var blocks = this.getSourceBlock().workspace.getAllBlocks(false); for (var i = 0; i < blocks.length; i++) { if (blocks[i].renameProcedure) { - blocks[i].renameProcedure(oldName, legalName); + var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( + blocks[i]); + procedureBlock.renameProcedure( + /** @type {string} */ (oldName), legalName); } } } @@ -253,6 +277,77 @@ Blockly.Procedures.flyoutCategory = function(workspace) { return xmlList; }; +/** + * Updates the procedure mutator's flyout so that the arg block is not a + * duplicate of another arg. + * @param {!Blockly.Workspace} workspace The procedure mutator's workspace. This + * workspace's flyout is what is being updated. + * @private + */ +Blockly.Procedures.updateMutatorFlyout_ = function(workspace) { + var usedNames = []; + var blocks = workspace.getBlocksByType('procedures_mutatorarg', false); + for (var i = 0, block; (block = blocks[i]); i++) { + usedNames.push(block.getFieldValue('NAME')); + } + + var xml = Blockly.utils.xml.createElement('xml'); + var argBlock = Blockly.utils.xml.createElement('block'); + argBlock.setAttribute('type', 'procedures_mutatorarg'); + var nameField = Blockly.utils.xml.createElement('field'); + nameField.setAttribute('name', 'NAME'); + var argValue = Blockly.Variables.generateUniqueNameFromOptions( + Blockly.Procedures.DEFAULT_ARG, usedNames); + var fieldContent = Blockly.utils.xml.createTextNode(argValue); + + nameField.appendChild(fieldContent); + argBlock.appendChild(nameField); + xml.appendChild(argBlock); + + workspace.updateToolbox(xml); +}; + +/** + * Listens for when a procedure mutator is opened. Then it triggers a flyout + * update and adds a mutator change listener to the mutator workspace. + * @param {!Blockly.Events.Abstract} e The event that triggered this listener. + * @package + */ +Blockly.Procedures.mutatorOpenListener = function(e) { + if (e.type != Blockly.Events.UI || e.element != 'mutatorOpen' || + !e.newValue) { + return; + } + var workspaceId = /** @type {string} */ (e.workspaceId); + var block = Blockly.Workspace.getById(workspaceId) + .getBlockById(e.blockId); + var type = block.type; + if (type != 'procedures_defnoreturn' && type != 'procedures_defreturn') { + return; + } + var workspace = block.mutator.getWorkspace(); + Blockly.Procedures.updateMutatorFlyout_(workspace); + workspace.addChangeListener(Blockly.Procedures.mutatorChangeListener_); +}; + +/** + * Listens for changes in a procedure mutator and triggers flyout updates when + * necessary. + * @param {!Blockly.Events.Abstract} e The event that triggered this listener. + * @private + */ +Blockly.Procedures.mutatorChangeListener_ = function(e) { + if (e.type != Blockly.Events.BLOCK_CREATE && + e.type != Blockly.Events.BLOCK_DELETE && + e.type != Blockly.Events.BLOCK_CHANGE) { + return; + } + var workspaceId = /** @type {string} */ (e.workspaceId); + var workspace = /** @type {!Blockly.WorkspaceSvg} */ + (Blockly.Workspace.getById(workspaceId)); + Blockly.Procedures.updateMutatorFlyout_(workspace); +}; + /** * Find all the callers of a named procedure. * @param {string} name Name of procedure. @@ -265,7 +360,9 @@ Blockly.Procedures.getCallers = function(name, workspace) { // Iterate through every block and check the name. for (var i = 0; i < blocks.length; i++) { if (blocks[i].getProcedureCall) { - var procName = blocks[i].getProcedureCall(); + var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( + blocks[i]); + var procName = procedureBlock.getProcedureCall(); // Procedure name may be null if the block is only half-built. if (procName && Blockly.Names.equals(procName, name)) { callers.push(blocks[i]); @@ -282,10 +379,12 @@ Blockly.Procedures.getCallers = function(name, workspace) { */ Blockly.Procedures.mutateCallers = function(defBlock) { var oldRecordUndo = Blockly.Events.recordUndo; - var name = defBlock.getProcedureDef()[0]; + var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( + defBlock); + var name = procedureBlock.getProcedureDef()[0]; var xmlElement = defBlock.mutationToDom(true); var callers = Blockly.Procedures.getCallers(name, defBlock.workspace); - for (var i = 0, caller; caller = callers[i]; i++) { + for (var i = 0, caller; (caller = callers[i]); i++) { var oldMutationDom = caller.mutationToDom(); var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom); caller.domToMutation(xmlElement); @@ -314,7 +413,9 @@ Blockly.Procedures.getDefinition = function(name, workspace) { var blocks = workspace.getTopBlocks(false); for (var i = 0; i < blocks.length; i++) { if (blocks[i].getProcedureDef) { - var tuple = blocks[i].getProcedureDef(); + var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ ( + blocks[i]); + var tuple = procedureBlock.getProcedureDef(); if (tuple && Blockly.Names.equals(tuple[0], name)) { return blocks[i]; } diff --git a/core/rendered_connection.js b/core/rendered_connection.js index 76c2fa278..5ede9de0e 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -65,31 +65,65 @@ Blockly.RenderedConnection = function(source, type) { this.offsetInBlock_ = new Blockly.utils.Coordinate(0, 0); /** - * Has this connection been added to the connection database? - * @type {boolean} + * Describes the state of this connection's tracked-ness. + * @type {Blockly.RenderedConnection.TrackedState} * @private */ - this.inDB_ = false; - - /** - * Whether this connections is hidden (not tracked in a database) or not. - * @type {boolean} - * @private - */ - this.hidden_ = !this.db_; + this.trackedState_ = Blockly.RenderedConnection.TrackedState.WILL_TRACK; }; Blockly.utils.object.inherits(Blockly.RenderedConnection, Blockly.Connection); /** + * Enum for different kinds of tracked states. + * + * WILL_TRACK means that this connection will add itself to + * the db on the next moveTo call it receives. + * + * UNTRACKED means that this connection will not add + * itself to the database until setTracking(true) is explicitly called. + * + * TRACKED means that this connection is currently being tracked. + * @enum {number} + */ +Blockly.RenderedConnection.TrackedState = { + WILL_TRACK: -1, + UNTRACKED: 0, + TRACKED: 1 +}; + +/** + * Dispose of this connection. Remove it from the database (if it is + * tracked) and call the super-function to deal with connected blocks. * @override + * @package */ Blockly.RenderedConnection.prototype.dispose = function() { Blockly.RenderedConnection.superClass_.dispose.call(this); - if (this.inDB_) { - this.db_.removeConnection_(this); + if (this.trackedState_ == Blockly.RenderedConnection.TrackedState.TRACKED) { + this.db_.removeConnection(this, this.y); } }; +/** + * Get the source block for this connection. + * @return {!Blockly.BlockSvg} The source block. + * @override + */ +Blockly.RenderedConnection.prototype.getSourceBlock = function() { + return /** @type {!Blockly.BlockSvg} */ ( + Blockly.RenderedConnection.superClass_.getSourceBlock.call(this)); +}; + +/** + * Returns the block that this connection connects to. + * @return {Blockly.BlockSvg} The connected block or null if none is connected. + * @override + */ +Blockly.RenderedConnection.prototype.targetBlock = function() { + return /** @type {Blockly.BlockSvg} */ ( + Blockly.RenderedConnection.superClass_.targetBlock.call(this)); +}; + /** * Returns the distance between this connection and another connection in * workspace units. @@ -98,8 +132,8 @@ Blockly.RenderedConnection.prototype.dispose = function() { * @return {number} The distance between connections, in workspace units. */ Blockly.RenderedConnection.prototype.distanceFrom = function(otherConnection) { - var xDiff = this.x_ - otherConnection.x_; - var yDiff = this.y_ - otherConnection.y_; + var xDiff = this.x - otherConnection.x; + var yDiff = this.y - otherConnection.y; return Math.sqrt(xDiff * xDiff + yDiff * yDiff); }; @@ -108,9 +142,9 @@ Blockly.RenderedConnection.prototype.distanceFrom = function(otherConnection) { * visually interfere with the specified connection. * @param {!Blockly.Connection} staticConnection The connection to move away * from. - * @private + * @package */ -Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) { +Blockly.RenderedConnection.prototype.bumpAwayFrom = function(staticConnection) { if (this.sourceBlock_.workspace.isDragging()) { // Don't move blocks around while the user is doing the same. return; @@ -136,17 +170,17 @@ Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) // Raise it to the top for extra visibility. var selected = Blockly.selected == rootBlock; selected || rootBlock.addSelect(); - var dx = (staticConnection.x_ + Blockly.SNAP_RADIUS + - Math.floor(Math.random() * Blockly.BUMP_RANDOMNESS)) - this.x_; - var dy = (staticConnection.y_ + Blockly.SNAP_RADIUS + - Math.floor(Math.random() * Blockly.BUMP_RANDOMNESS)) - this.y_; + var dx = (staticConnection.x + Blockly.SNAP_RADIUS + + Math.floor(Math.random() * Blockly.BUMP_RANDOMNESS)) - this.x; + var dy = (staticConnection.y + Blockly.SNAP_RADIUS + + Math.floor(Math.random() * Blockly.BUMP_RANDOMNESS)) - this.y; if (reverse) { // When reversing a bump due to an uneditable block, bump up. dy = -dy; } if (rootBlock.RTL) { - dx = (staticConnection.x_ - Blockly.SNAP_RADIUS - - Math.floor(Math.random() * Blockly.BUMP_RANDOMNESS)) - this.x_; + dx = (staticConnection.x - Blockly.SNAP_RADIUS - + Math.floor(Math.random() * Blockly.BUMP_RANDOMNESS)) - this.x; } rootBlock.moveBy(dx, dy); selected || rootBlock.removeSelect(); @@ -158,16 +192,16 @@ Blockly.RenderedConnection.prototype.bumpAwayFrom_ = function(staticConnection) * @param {number} y New absolute y coordinate, in workspace coordinates. */ Blockly.RenderedConnection.prototype.moveTo = function(x, y) { - // Remove it from its old location in the database (if already present) - if (this.inDB_) { - this.db_.removeConnection_(this); - } - this.x_ = x; - this.y_ = y; - // Insert it into its new location in the database. - if (!this.hidden_) { - this.db_.addConnection(this); + if (this.trackedState_ == Blockly.RenderedConnection.TrackedState.WILL_TRACK) { + this.db_.addConnection(this, y); + this.trackedState_ = Blockly.RenderedConnection.TrackedState.TRACKED; + } else if (this.trackedState_ == Blockly.RenderedConnection + .TrackedState.TRACKED) { + this.db_.removeConnection(this, this.y); + this.db_.addConnection(this, y); } + this.x = x; + this.y = y; }; /** @@ -176,14 +210,14 @@ Blockly.RenderedConnection.prototype.moveTo = function(x, y) { * @param {number} dy Change to y coordinate, in workspace units. */ Blockly.RenderedConnection.prototype.moveBy = function(dx, dy) { - this.moveTo(this.x_ + dx, this.y_ + dy); + this.moveTo(this.x + dx, this.y + dy); }; /** * Move this connection to the location given by its offset within the block and * the location of the block's top left corner. - * @param {!Blockly.utils.Coordinate} blockTL The location of the top left corner - * of the block, in workspace coordinates. + * @param {!Blockly.utils.Coordinate} blockTL The location of the top left + * corner of the block, in workspace coordinates. */ Blockly.RenderedConnection.prototype.moveToOffset = function(blockTL) { this.moveTo(blockTL.x + this.offsetInBlock_.x, @@ -211,11 +245,11 @@ Blockly.RenderedConnection.prototype.getOffsetInBlock = function() { /** * Move the blocks on either side of this connection right next to each other. - * @private + * @package */ -Blockly.RenderedConnection.prototype.tighten_ = function() { - var dx = this.targetConnection.x_ - this.x_; - var dy = this.targetConnection.y_ - this.y_; +Blockly.RenderedConnection.prototype.tighten = function() { + var dx = this.targetConnection.x - this.x; + var dy = this.targetConnection.y - this.y; if (dx != 0 || dy != 0) { var block = this.targetBlock(); var svgRoot = block.getSvgRoot(); @@ -226,7 +260,7 @@ Blockly.RenderedConnection.prototype.tighten_ = function() { var xy = Blockly.utils.getRelativeXY(svgRoot); block.getSvgRoot().setAttribute('transform', 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')'); - block.moveConnections_(-dx, -dy); + block.moveConnections(-dx, -dy); } }; @@ -250,26 +284,27 @@ Blockly.RenderedConnection.prototype.closest = function(maxLimit, dxy) { Blockly.RenderedConnection.prototype.highlight = function() { var steps; var sourceBlockSvg = /** @type {!Blockly.BlockSvg} */ (this.sourceBlock_); - var renderingConstants = - sourceBlockSvg.workspace.getRenderer().getConstants(); + var renderConstants = sourceBlockSvg.workspace.getRenderer().getConstants(); + var shape = renderConstants.shapeFor(this); if (this.type == Blockly.INPUT_VALUE || this.type == Blockly.OUTPUT_VALUE) { // Vertical line, puzzle tab, vertical line. - var yLen = 5; + var yLen = renderConstants.TAB_OFFSET_FROM_TOP; steps = Blockly.utils.svgPaths.moveBy(0, -yLen) + Blockly.utils.svgPaths.lineOnAxis('v', yLen) + - renderingConstants.PUZZLE_TAB.pathDown + + shape.pathDown + Blockly.utils.svgPaths.lineOnAxis('v', yLen); } else { - var xLen = 5; + var xLen = + renderConstants.NOTCH_OFFSET_LEFT - renderConstants.CORNER_RADIUS; // Horizontal line, notch, horizontal line. steps = Blockly.utils.svgPaths.moveBy(-xLen, 0) + Blockly.utils.svgPaths.lineOnAxis('h', xLen) + - renderingConstants.NOTCH.pathLeft + + shape.pathLeft + Blockly.utils.svgPaths.lineOnAxis('h', xLen); } var xy = this.sourceBlock_.getRelativeToSurfaceXY(); - var x = this.x_ - xy.x; - var y = this.y_ - xy.y; + var x = this.x - xy.x; + var y = this.y - xy.y; Blockly.Connection.highlightedPath_ = Blockly.utils.dom.createSvgElement( 'path', { @@ -282,17 +317,79 @@ Blockly.RenderedConnection.prototype.highlight = function() { }; /** - * Unhide this connection, as well as all down-stream connections on any block - * attached to this connection. This happens when a block is expanded. - * Also unhides down-stream comments. + * Remove the highlighting around this connection. + */ +Blockly.RenderedConnection.prototype.unhighlight = function() { + Blockly.utils.dom.removeNode(Blockly.Connection.highlightedPath_); + delete Blockly.Connection.highlightedPath_; +}; + +/** + * Set whether this connections is tracked in the database or not. + * @param {boolean} doTracking If true, start tracking. If false, stop tracking. + * @package + */ +Blockly.RenderedConnection.prototype.setTracking = function(doTracking) { + if ((doTracking && this.trackedState_ == + Blockly.RenderedConnection.TrackedState.TRACKED) || + (!doTracking && this.trackedState_ == + Blockly.RenderedConnection.TrackedState.UNTRACKED)) { + return; + } + if (this.sourceBlock_.isInFlyout) { + // Don't bother maintaining a database of connections in a flyout. + return; + } + if (doTracking) { + this.db_.addConnection(this, this.y); + this.trackedState_ = Blockly.RenderedConnection.TrackedState.TRACKED; + return; + } + if (this.trackedState_ == Blockly.RenderedConnection.TrackedState.TRACKED) { + this.db_.removeConnection(this, this.y); + } + this.trackedState_ = Blockly.RenderedConnection.TrackedState.UNTRACKED; +}; + +/** + * Stop tracking this connection, as well as all down-stream connections on + * any block attached to this connection. This happens when a block is + * collapsed. + * + * Also closes down-stream icons/bubbles. + * @package + */ +Blockly.RenderedConnection.prototype.stopTrackingAll = function() { + this.setTracking(false); + if (this.targetConnection) { + var blocks = this.targetBlock().getDescendants(false); + for (var i = 0; i < blocks.length; i++) { + var block = blocks[i]; + // Stop tracking connections of all children. + var connections = block.getConnections_(true); + for (var j = 0; j < connections.length; j++) { + connections[j].setTracking(false); + } + // Close all bubbles of all children. + var icons = block.getIcons(); + for (var j = 0; j < icons.length; j++) { + icons[j].setVisible(false); + } + } + } +}; + +/** + * Start tracking this connection, as well as all down-stream connections on + * any block attached to this connection. This happens when a block is expanded. * @return {!Array.} List of blocks to render. */ -Blockly.RenderedConnection.prototype.unhideAll = function() { - this.setHidden(false); - // All blocks that need unhiding must be unhidden before any rendering takes - // place, since rendering requires knowing the dimensions of lower blocks. - // Also, since rendering a block renders all its parents, we only need to - // render the leaf nodes. +Blockly.RenderedConnection.prototype.startTrackingAll = function() { + this.setTracking(true); + // All blocks that are not tracked must start tracking before any + // rendering takes place, since rendering requires knowing the dimensions + // of lower blocks. Also, since rendering a block renders all its parents, + // we only need to render the leaf nodes. var renderList = []; if (this.type != Blockly.INPUT_VALUE && this.type != Blockly.NEXT_STATEMENT) { // Only spider down. @@ -312,7 +409,7 @@ Blockly.RenderedConnection.prototype.unhideAll = function() { connections = block.getConnections_(true); } for (var i = 0; i < connections.length; i++) { - renderList.push.apply(renderList, connections[i].unhideAll()); + renderList.push.apply(renderList, connections[i].startTrackingAll()); } if (!renderList.length) { // Leaf block. @@ -322,52 +419,6 @@ Blockly.RenderedConnection.prototype.unhideAll = function() { return renderList; }; -/** - * Remove the highlighting around this connection. - */ -Blockly.RenderedConnection.prototype.unhighlight = function() { - Blockly.utils.dom.removeNode(Blockly.Connection.highlightedPath_); - delete Blockly.Connection.highlightedPath_; -}; - -/** - * Set whether this connections is hidden (not tracked in a database) or not. - * @param {boolean} hidden True if connection is hidden. - */ -Blockly.RenderedConnection.prototype.setHidden = function(hidden) { - this.hidden_ = hidden; - if (hidden && this.inDB_) { - this.db_.removeConnection_(this); - } else if (!hidden && !this.inDB_) { - this.db_.addConnection(this); - } -}; - -/** - * Hide this connection, as well as all down-stream connections on any block - * attached to this connection. This happens when a block is collapsed. - * Also hides down-stream comments. - */ -Blockly.RenderedConnection.prototype.hideAll = function() { - this.setHidden(true); - if (this.targetConnection) { - var blocks = this.targetBlock().getDescendants(false); - for (var i = 0; i < blocks.length; i++) { - var block = blocks[i]; - // Hide all connections of all children. - var connections = block.getConnections_(true); - for (var j = 0; j < connections.length; j++) { - connections[j].setHidden(true); - } - // Close all bubbles of all children. - var icons = block.getIcons(); - for (var j = 0; j < icons.length; j++) { - icons[j].setVisible(false); - } - } - } -}; - /** * Check if the two connections can be dragged to connect to each other. * @param {!Blockly.Connection} candidate A nearby connection to check. @@ -387,13 +438,13 @@ Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate, /** * Behavior after a connection attempt fails. - * @param {Blockly.Connection} otherConnection Connection that this connection + * @param {!Blockly.Connection} otherConnection Connection that this connection * failed to connect to. * @package */ Blockly.RenderedConnection.prototype.onFailedConnect = function( otherConnection) { - this.bumpAwayFrom_(otherConnection); + this.bumpAwayFrom(otherConnection); }; @@ -446,9 +497,9 @@ Blockly.RenderedConnection.prototype.respawnShadow_ = function() { * @param {number} maxLimit The maximum radius to another connection, in * workspace units. * @return {!Array.} List of connections. - * @private + * @package */ -Blockly.RenderedConnection.prototype.neighbours_ = function(maxLimit) { +Blockly.RenderedConnection.prototype.neighbours = function(maxLimit) { return this.dbOpposite_.getNeighbours(this, maxLimit); }; @@ -456,7 +507,7 @@ Blockly.RenderedConnection.prototype.neighbours_ = function(maxLimit) { * Connect two connections together. This is the connection on the superior * block. Rerender blocks as needed. * @param {!Blockly.Connection} childConnection Connection on inferior block. - * @private + * @protected */ Blockly.RenderedConnection.prototype.connect_ = function(childConnection) { Blockly.RenderedConnection.superClass_.connect_.call(this, childConnection); @@ -487,11 +538,12 @@ Blockly.RenderedConnection.prototype.connect_ = function(childConnection) { /** * Function to be called when this connection's compatible types have changed. - * @private + * @protected */ Blockly.RenderedConnection.prototype.onCheckChanged_ = function() { // The new value type may not be compatible with the existing connection. - if (this.isConnected() && !this.checkType_(this.targetConnection)) { + if (this.isConnected() && (!this.targetConnection || + !this.checkType(this.targetConnection))) { var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; child.unplug(); // Bump away. diff --git a/core/renderers/common/block_rendering.js b/core/renderers/common/block_rendering.js index 794a83717..da3a71f6a 100644 --- a/core/renderers/common/block_rendering.js +++ b/core/renderers/common/block_rendering.js @@ -101,15 +101,16 @@ Blockly.blockRendering.init = function(name) { /** * Wrap the renderer constructor into a temporary constructor * function so the closure compiler treats it as a constructor. + * @param {string} name The renderer name. * @constructor * @extends {Blockly.blockRendering.Renderer} */ - var rendererCtor = function() { - rendererCtor.superClass_.constructor.call(this); + var rendererCtor = function(name) { + rendererCtor.superClass_.constructor.call(this, name); }; Blockly.utils.object.inherits(rendererCtor, Blockly.blockRendering.rendererMap_[name]); - var renderer = new rendererCtor(); + var renderer = new rendererCtor(name); renderer.init(); return renderer; }; diff --git a/core/renderers/common/constants.js b/core/renderers/common/constants.js index 7e210db36..654208612 100644 --- a/core/renderers/common/constants.js +++ b/core/renderers/common/constants.js @@ -23,7 +23,11 @@ goog.provide('Blockly.blockRendering.ConstantProvider'); +goog.require('Blockly.utils'); +goog.require('Blockly.utils.colour'); +goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.svgPaths'); +goog.require('Blockly.utils.userAgent'); /** @@ -32,30 +36,86 @@ goog.require('Blockly.utils.svgPaths'); * @package */ Blockly.blockRendering.ConstantProvider = function() { + + /** + * The size of an empty spacer. + * @type {number} + */ this.NO_PADDING = 0; + + /** + * The size of small padding. + * @type {number} + */ this.SMALL_PADDING = 3; + + /** + * The size of medium padding. + * @type {number} + */ this.MEDIUM_PADDING = 5; + + /** + * The size of medium-large padding. + * @type {number} + */ this.MEDIUM_LARGE_PADDING = 8; + + /** + * The size of large padding. + * @type {number} + */ this.LARGE_PADDING = 10; - // Offset from the top of the row for placing fields on inline input rows - // and statement input rows. - // Matches existing rendering (in 2019). + /** + * Offset from the top of the row for placing fields on inline input rows + * and statement input rows. + * Matches existing rendering (in 2019). + * @type {number} + */ this.TALL_INPUT_FIELD_OFFSET_Y = this.MEDIUM_PADDING; + /** + * The height of the puzzle tab used for input and output connections. + * @type {number} + */ this.TAB_HEIGHT = 15; + /** + * The offset from the top of the block at which a puzzle tab is positioned. + * @type {number} + */ this.TAB_OFFSET_FROM_TOP = 5; + /** + * Vertical overlap of the puzzle tab, used to make it look more like a puzzle + * piece. + * @type {number} + */ this.TAB_VERTICAL_OVERLAP = 2.5; + /** + * The width of the puzzle tab used for input and output connections. + * @type {number} + */ this.TAB_WIDTH = 8; + /** + * The width of the notch used for previous and next connections. + * @type {number} + */ this.NOTCH_WIDTH = 15; + + /** + * The height of the notch used for previous and next connections. + * @type {number} + */ this.NOTCH_HEIGHT = 4; - // This is the minimum width of a block measuring from the end of a rounded - // corner + /** + * The minimum width of the block. + * @type {number} + */ this.MIN_BLOCK_WIDTH = 12; this.EMPTY_BLOCK_SPACER_HEIGHT = 16; @@ -66,35 +126,81 @@ Blockly.blockRendering.ConstantProvider = function() { */ this.DUMMY_INPUT_MIN_HEIGHT = this.TAB_HEIGHT; + /** + * The minimum height of a dummy input row in a shadow block. + * @type {number} + */ + this.DUMMY_INPUT_SHADOW_MIN_HEIGHT = this.TAB_HEIGHT; + /** * Rounded corner radius. * @type {number} */ this.CORNER_RADIUS = 8; - // Offset from the left side of a block or the inside of a statement input to - // the left side of the notch. + /** + * Offset from the left side of a block or the inside of a statement input to + * the left side of the notch. + * @type {number} + */ this.NOTCH_OFFSET_LEFT = 15; + /** + * Additional offset added to the statement input's width to account for the + * notch. + * @type {number} + */ + this.STATEMENT_INPUT_NOTCH_OFFSET = this.NOTCH_OFFSET_LEFT; + this.STATEMENT_BOTTOM_SPACER = 0; this.STATEMENT_INPUT_PADDING_LEFT = 20; + + /** + * Vertical padding between consecutive statement inputs. + * @type {number} + */ this.BETWEEN_STATEMENT_PADDING_Y = 4; - // This is the max width of a bottom row that follows a statement input and - // has inputs inline. - this.MAX_BOTTOM_WIDTH = 66.5; + /** + * The top row's minimum height. + * @type {number} + */ + this.TOP_ROW_MIN_HEIGHT = this.MEDIUM_PADDING; + + /** + * The top row's minimum height if it precedes a statement. + * @type {number} + */ + this.TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT = this.LARGE_PADDING; + + /** + * The bottom row's minimum height. + * @type {number} + */ + this.BOTTOM_ROW_MIN_HEIGHT = this.MEDIUM_PADDING; + + /** + * The bottom row's minimum height if it follows a statement input. + * @type {number} + */ + this.BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT = this.LARGE_PADDING; + + /** + * Whether to add a 'hat' on top of all blocks with no previous or output + * connections. Can be overridden by 'hat' property on Theme.BlockStyle. + * @type {boolean} + */ + this.ADD_START_HATS = false; /** * Height of the top hat. - * @const - * @private + * @type {number} */ this.START_HAT_HEIGHT = 15; /** * Width of the top hat. - * @const - * @private + * @type {number} */ this.START_HAT_WIDTH = 100; @@ -104,6 +210,10 @@ Blockly.blockRendering.ConstantProvider = function() { this.EMPTY_INLINE_INPUT_PADDING = 14.5; + /** + * The height of an empty inline input. + * @type {number} + */ this.EMPTY_INLINE_INPUT_HEIGHT = this.TAB_HEIGHT + 11; this.EXTERNAL_VALUE_INPUT_PADDING = 2; @@ -113,7 +223,6 @@ Blockly.blockRendering.ConstantProvider = function() { * varies slightly depending on whether the block has external or inline inputs. * In the new rendering this is consistent. It seems unlikely that the old * behaviour was intentional. - * @const * @type {number} */ this.EMPTY_STATEMENT_INPUT_HEIGHT = this.MIN_BLOCK_HEIGHT; @@ -122,15 +231,290 @@ Blockly.blockRendering.ConstantProvider = function() { /** * Height of SVG path for jagged teeth at the end of collapsed blocks. - * @const + * @type {number} */ this.JAGGED_TEETH_HEIGHT = 12; /** * Width of SVG path for jagged teeth at the end of collapsed blocks. - * @const + * @type {number} */ this.JAGGED_TEETH_WIDTH = 6; + + /** + * Point size of text. + * @type {number} + */ + this.FIELD_TEXT_FONTSIZE = 11; + + /** + * Height of text. + * @type {number} + */ + this.FIELD_TEXT_HEIGHT = 16; + + /** + * Text font weight. + * @type {string} + */ + this.FIELD_TEXT_FONTWEIGHT = 'normal'; + + /** + * Text font family. + * @type {string} + */ + this.FIELD_TEXT_FONTFAMILY = 'sans-serif'; + + /** + * A field's border rect corner radius. + * @type {number} + */ + this.FIELD_BORDER_RECT_RADIUS = 4; + + /** + * A field's border rect default height. + * @type {number} + */ + this.FIELD_BORDER_RECT_HEIGHT = 16; + + /** + * A field's border rect X padding. + * @type {number} + */ + this.FIELD_BORDER_RECT_X_PADDING = 5; + + /** + * A field's border rect Y padding. + * @type {number} + */ + this.FIELD_BORDER_RECT_Y_PADDING = 3; + + /** + * The backing colour of a field's border rect. + * @type {string} + * @package + */ + this.FIELD_BORDER_RECT_COLOUR = '#fff'; + + /** + * Field text baseline. + * This is only used if `FIELD_TEXT_BASELINE_CENTER` is false. + * @type {number} + */ + this.FIELD_TEXT_BASELINE_Y = Blockly.utils.userAgent.GECKO ? 12 : 13.09; + + /** + * An text offset adjusting the Y position of text after positioning. + * @type {number} + */ + this.FIELD_TEXT_Y_OFFSET = 0; + + /** + * A field's text element's dominant baseline. + * @type {boolean} + */ + this.FIELD_TEXT_BASELINE_CENTER = + !Blockly.utils.userAgent.IE && !Blockly.utils.userAgent.EDGE; + + /** + * A dropdown field's border rect height. + * @type {number} + */ + this.FIELD_DROPDOWN_BORDER_RECT_HEIGHT = this.FIELD_BORDER_RECT_HEIGHT; + + /** + * Whether or not a dropdown field should add a border rect when in a shadow + * block. + * @type {boolean} + */ + this.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW = false; + + /** + * Whether or not a dropdown field's div should be coloured to match the + * block colours. + * @type {boolean} + */ + this.FIELD_DROPDOWN_COLOURED_DIV = false; + + /** + * Whether or not a dropdown field uses a text or SVG arrow. + * @type {boolean} + */ + this.FIELD_DROPDOWN_SVG_ARROW = false; + + /** + * A dropdown field's SVG arrow padding. + * @type {number} + */ + this.FIELD_DROPDOWN_SVG_ARROW_PADDING = this.FIELD_BORDER_RECT_X_PADDING; + + /** + * A dropdown field's SVG arrow size. + * @type {number} + */ + this.FIELD_DROPDOWN_SVG_ARROW_SIZE = 12; + + /** + * A dropdown field's SVG arrow datauri. + * @type {string} + */ + this.FIELD_DROPDOWN_SVG_ARROW_DATAURI = + 'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllci' + + 'AxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMi43MSIgaG' + + 'VpZ2h0PSI4Ljc5IiB2aWV3Qm94PSIwIDAgMTIuNzEgOC43OSI+PHRpdGxlPmRyb3Bkb3duLW' + + 'Fycm93PC90aXRsZT48ZyBvcGFjaXR5PSIwLjEiPjxwYXRoIGQ9Ik0xMi43MSwyLjQ0QTIuND' + + 'EsMi40MSwwLDAsMSwxMiw0LjE2TDguMDgsOC4wOGEyLjQ1LDIuNDUsMCwwLDEtMy40NSwwTD' + + 'AuNzIsNC4xNkEyLjQyLDIuNDIsMCwwLDEsMCwyLjQ0LDIuNDgsMi40OCwwLDAsMSwuNzEuNz' + + 'FDMSwwLjQ3LDEuNDMsMCw2LjM2LDBTMTEuNzUsMC40NiwxMiwuNzFBMi40NCwyLjQ0LDAsMC' + + 'wxLDEyLjcxLDIuNDRaIiBmaWxsPSIjMjMxZjIwIi8+PC9nPjxwYXRoIGQ9Ik02LjM2LDcuNz' + + 'lhMS40MywxLjQzLDAsMCwxLTEtLjQyTDEuNDIsMy40NWExLjQ0LDEuNDQsMCwwLDEsMC0yYz' + + 'AuNTYtLjU2LDkuMzEtMC41Niw5Ljg3LDBhMS40NCwxLjQ0LDAsMCwxLDAsMkw3LjM3LDcuMz' + + 'dBMS40MywxLjQzLDAsMCwxLDYuMzYsNy43OVoiIGZpbGw9IiNmZmYiLz48L3N2Zz4='; + + /** + * Whether or not to show a box shadow around the widget div. This is only a + * feature of full block fields. + * @type {boolean} + */ + this.FIELD_TEXTINPUT_BOX_SHADOW = false; + + /** + * Whether or not the colour field should display its colour value on the + * entire block. + * @type {boolean} + */ + this.FIELD_COLOUR_FULL_BLOCK = false; + + /** + * A colour field's default width. + * @type {number} + */ + this.FIELD_COLOUR_DEFAULT_WIDTH = 26; + + /** + * A colour field's default height. + * @type {number} + */ + this.FIELD_COLOUR_DEFAULT_HEIGHT = this.FIELD_BORDER_RECT_HEIGHT; + + /** + * A checkbox field's X offset. + * @type {number} + */ + this.FIELD_CHECKBOX_X_OFFSET = this.FIELD_BORDER_RECT_X_PADDING - 3; + + /** + * A checkbox field's Y offset. + * @type {number} + */ + this.FIELD_CHECKBOX_Y_OFFSET = 14; + + /** + * A checkbox field's default width. + * @type {number} + */ + this.FIELD_CHECKBOX_DEFAULT_WIDTH = 15; + + /** + * A random identifier used to ensure a unique ID is used for each + * filter/pattern for the case of multiple Blockly instances on a page. + * @type {string} + * @protected + */ + this.randomIdentifier_ = String(Math.random()).substring(2); + + /** + * The ID of the emboss filter, or the empty string if no filter is set. + * @type {string} + * @package + */ + this.embossFilterId = ''; + + /** + * The element to use for highlighting, or null if not set. + * @type {SVGElement} + * @private + */ + this.embossFilter_ = null; + + /** + * The ID of the disabled pattern, or the empty string if no pattern is set. + * @type {string} + * @package + */ + this.disabledPatternId = ''; + + /** + * The element to use for disabled blocks, or null if not set. + * @type {SVGElement} + * @private + */ + this.disabledPattern_ = null; + + /** + * Cursor colour. + * @type {string} + * @package + */ + this.CURSOR_COLOUR = '#cc0a0a'; + + /** + * Immovable marker colour. + * @type {string} + * @package + */ + this.MARKER_COLOUR = '#4286f4'; + + /** + * Width of the horizontal cursor. + * @type {number} + * @package + */ + this.CURSOR_WS_WIDTH = 100; + + /** + * Height of the horizontal cursor. + * @type {number} + * @package + */ + this.WS_CURSOR_HEIGHT = 5; + + /** + * Padding around a stack. + * @type {number} + * @package + */ + this.CURSOR_STACK_PADDING = 10; + + /** + * Padding around a block. + * @type {number} + * @package + */ + this.CURSOR_BLOCK_PADDING = 2; + + /** + * Stroke of the cursor. + * @type {number} + * @package + */ + this.CURSOR_STROKE_WIDTH = 4; + + /** + * Whether text input and colour fields fill up the entire source block. + * @type {boolean} + * @package + */ + this.FULL_BLOCK_FIELDS = false; + + /** + * Enum for connection shapes. + * @enum {number} + */ + this.SHAPES = { + PUZZLE: 1, + NOTCH: 2 + }; }; /** @@ -177,6 +561,147 @@ Blockly.blockRendering.ConstantProvider.prototype.init = function() { this.OUTSIDE_CORNERS = this.makeOutsideCorners(); }; +/** + * Refresh constants properties that depend on the theme. + * @param {!Blockly.Theme} theme The current workspace theme. + * @package + */ +Blockly.blockRendering.ConstantProvider.prototype.refreshTheme = function( + theme) { + + /** + * The block styles map. + * @type {Object.} + * @package + */ + this.blockStyles = {}; + + var blockStyles = theme.blockStyles; + for (var key in blockStyles) { + this.blockStyles[key] = this.validatedBlockStyle_(blockStyles[key]); + } +}; + +/** + * Get or create a block style based on a single colour value. Generate a name + * for the style based on the colour. + * @param {string} colour #RRGGBB colour string. + * @return {{style: !Blockly.Theme.BlockStyle, name: string}} An object + * containing the style and an autogenerated name for that style. + * @package + */ +Blockly.blockRendering.ConstantProvider.prototype.getBlockStyleForColour = + function(colour) { + /* eslint-disable indent */ + var name = 'auto_' + colour; + if (!this.blockStyles[name]) { + this.blockStyles[name] = this.createBlockStyle_(colour); + } + return {style: this.blockStyles[name], name: name}; +}; /* eslint-enable indent */ + +/** + * Gets the BlockStyle for the given block style name. + * @param {?string} blockStyleName The name of the block style. + * @return {!Blockly.Theme.BlockStyle} The named block style, or a default style + * if no style with the given name was found. + */ +Blockly.blockRendering.ConstantProvider.prototype.getBlockStyle = function( + blockStyleName) { + return this.blockStyles[blockStyleName || ''] || + this.createBlockStyle_('#000000'); +}; + +/** + * Create a block style object based on the given colour. + * @param {string} colour #RRGGBB colour string. + * @return {!Blockly.Theme.BlockStyle} A populated block style based on the + * given colour. + * @protected + */ +Blockly.blockRendering.ConstantProvider.prototype.createBlockStyle_ = function( + colour) { + return this.validatedBlockStyle_({ + 'colourPrimary': colour + }); +}; + +/** + * Get a full block style object based on the input style object. Populate + * any missing values. + * @param {{ + * colourPrimary:string, + * colourSecondary:(string|undefined), + * colourTertiary:(string|undefined), + * hat:(string|undefined) + * }} blockStyle A full or partial block style object. + + * @return {!Blockly.Theme.BlockStyle} A full block style object, with all + * required properties populated. + * @protected + */ +Blockly.blockRendering.ConstantProvider.prototype.validatedBlockStyle_ = + function(blockStyle) { + /* eslint-disable indent */ + // Make a new object with all of the same properties. + var valid = /** @type {!Blockly.Theme.BlockStyle} */ ({}); + if (blockStyle) { + Blockly.utils.object.mixin(valid, blockStyle); + } + // Validate required properties. + var parsedColour = Blockly.utils.parseBlockColour( + valid['colourPrimary'] || '#000'); + valid.colourPrimary = parsedColour.hex; + valid.colourSecondary = valid['colourSecondary'] ? + Blockly.utils.parseBlockColour(valid['colourSecondary']).hex : + this.generateSecondaryColour_(valid.colourPrimary); + valid.colourTertiary = valid['colourTertiary'] ? + Blockly.utils.parseBlockColour(valid['colourTertiary']).hex : + this.generateTertiaryColour_(valid.colourPrimary); + + valid.hat = valid['hat'] || ''; + return valid; +}; /* eslint-enable indent */ + +/** + * Generate a secondary colour from the passed in primary colour. + * @param {string} colour Primary colour. + * @return {string} The generated secondary colour. + * @protected + */ +Blockly.blockRendering.ConstantProvider.prototype.generateSecondaryColour_ = + function(colour) { + /* eslint-disable indent */ + return Blockly.utils.colour.blend('#fff', colour, 0.6) || colour; +}; /* eslint-enable indent */ + +/** + * Generate a tertiary colour from the passed in primary colour. + * @param {string} colour Primary colour. + * @return {string} The generated tertiary colour. + * @protected + */ +Blockly.blockRendering.ConstantProvider.prototype.generateTertiaryColour_ = + function(colour) { + /* eslint-disable indent */ + return Blockly.utils.colour.blend('#fff', colour, 0.3) || colour; +}; /* eslint-enable indent */ + + +/** + * Dispose of this constants provider. + * Delete all DOM elements that this provider created. + * @package + */ +Blockly.blockRendering.ConstantProvider.prototype.dispose = function() { + if (this.embossFilter_) { + Blockly.utils.dom.removeNode(this.embossFilter_); + } + if (this.disabledPattern_) { + Blockly.utils.dom.removeNode(this.disabledPattern_); + } +}; + /** * @return {!Object} An object containing sizing and path information about * collapsed block indicators. @@ -189,9 +714,9 @@ Blockly.blockRendering.ConstantProvider.prototype.makeJaggedTeeth = function() { var mainPath = Blockly.utils.svgPaths.line( [ - Blockly.utils.svgPaths.point(6, 3), - Blockly.utils.svgPaths.point(-12, 6), - Blockly.utils.svgPaths.point(6, 3) + Blockly.utils.svgPaths.point(width, height / 4), + Blockly.utils.svgPaths.point(-width * 2, height / 2), + Blockly.utils.svgPaths.point(width, height / 4) ]); return { height: height, @@ -269,6 +794,7 @@ Blockly.blockRendering.ConstantProvider.prototype.makePuzzleTab = function() { var pathDown = makeMainPath(false); return { + type: this.SHAPES.PUZZLE, width: width, height: height, pathDown: pathDown, @@ -294,11 +820,11 @@ Blockly.blockRendering.ConstantProvider.prototype.makeNotch = function() { Blockly.utils.svgPaths.point(dir * outerWidth, -height) ]); } - // TODO: Find a relationship between width and path var pathLeft = makeMainPath(1); var pathRight = makeMainPath(-1); return { + type: this.SHAPES.NOTCH, width: width, height: height, pathLeft: pathLeft, @@ -344,12 +870,34 @@ Blockly.blockRendering.ConstantProvider.prototype.makeOutsideCorners = function( Blockly.utils.svgPaths.arc('a', '0 0,1', radius, Blockly.utils.svgPaths.point(radius, -radius)); + /** + * SVG path for drawing the rounded top-right corner. + * @const + */ + var topRight = + Blockly.utils.svgPaths.arc('a', '0 0,1', radius, + Blockly.utils.svgPaths.point(radius, radius)); + + /** + * SVG path for drawing the rounded bottom-left corner. + * @const + */ var bottomLeft = Blockly.utils.svgPaths.arc('a', '0 0,1', radius, Blockly.utils.svgPaths.point(-radius, -radius)); + /** + * SVG path for drawing the rounded bottom-right corner. + * @const + */ + var bottomRight = Blockly.utils.svgPaths.arc('a', '0 0,1', radius, + Blockly.utils.svgPaths.point(-radius, radius)); + return { topLeft: topLeft, - bottomLeft: bottomLeft + topRight: topRight, + bottomRight: bottomRight, + bottomLeft: bottomLeft, + rightHeight: radius }; }; @@ -374,3 +922,172 @@ Blockly.blockRendering.ConstantProvider.prototype.shapeFor = function( throw Error('Unknown connection type'); } }; + +/** + * Create any DOM elements that this renderer needs (filters, patterns, etc). + * @param {!SVGElement} svg The root of the workspace's SVG. + * @package + */ +Blockly.blockRendering.ConstantProvider.prototype.createDom = function(svg) { + /* + + ... filters go here ... + + */ + var defs = Blockly.utils.dom.createSvgElement('defs', {}, svg); + /* + + + + + + + + + */ + var embossFilter = Blockly.utils.dom.createSvgElement('filter', + {'id': 'blocklyEmbossFilter' + this.randomIdentifier_}, defs); + Blockly.utils.dom.createSvgElement('feGaussianBlur', + {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter); + var feSpecularLighting = Blockly.utils.dom.createSvgElement('feSpecularLighting', + { + 'in': 'blur', + 'surfaceScale': 1, + 'specularConstant': 0.5, + 'specularExponent': 10, + 'lighting-color': 'white', + 'result': 'specOut' + }, + embossFilter); + Blockly.utils.dom.createSvgElement('fePointLight', + {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'specOut', + 'in2': 'SourceAlpha', + 'operator': 'in', + 'result': 'specOut' + }, embossFilter); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'SourceGraphic', + 'in2': 'specOut', + 'operator': 'arithmetic', + 'k1': 0, + 'k2': 1, + 'k3': 1, + 'k4': 0 + }, embossFilter); + this.embossFilterId = embossFilter.id; + this.embossFilter_ = embossFilter; + + /* + + + + + */ + var disabledPattern = Blockly.utils.dom.createSvgElement('pattern', + { + 'id': 'blocklyDisabledPattern' + this.randomIdentifier_, + 'patternUnits': 'userSpaceOnUse', + 'width': 10, + 'height': 10 + }, defs); + Blockly.utils.dom.createSvgElement('rect', + {'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern); + Blockly.utils.dom.createSvgElement('path', + {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern); + this.disabledPatternId = disabledPattern.id; + this.disabledPattern_ = disabledPattern; +}; + +/** + * Inject renderer specific CSS into the page. + * @param {string} name Name of the renderer. + * @package + */ +Blockly.blockRendering.ConstantProvider.prototype.injectCSS = function( + name) { + var cssArray = this.getCSS_(name); + var cssNodeId = 'blockly-renderer-style-' + name; + if (document.getElementById(cssNodeId)) { + // Already injected. + return; + } + var text = cssArray.join('\n'); + // Inject CSS tag at start of head. + var cssNode = document.createElement('style'); + cssNode.id = cssNodeId; + var cssTextNode = document.createTextNode(text); + cssNode.appendChild(cssTextNode); + document.head.insertBefore(cssNode, document.head.firstChild); +}; + +/** + * Get any renderer specific CSS to inject when the renderer is initialized. + * @param {string} name Name of the renderer. + * @return {!Array.} Array of CSS strings. + * @protected + */ +Blockly.blockRendering.ConstantProvider.prototype.getCSS_ = function(name) { + var selector = '.' + name + '-renderer'; + return [ + /* eslint-disable indent */ + // Fields. + selector + ' .blocklyText {', + 'fill: #fff;', + 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', + 'font-size: ' + this.FIELD_TEXT_FONTSIZE + 'pt;', + 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', + '}', + selector + ' .blocklyNonEditableText>rect,', + selector + ' .blocklyEditableText>rect {', + 'fill: ' + this.FIELD_BORDER_RECT_COLOUR + ';', + 'fill-opacity: .6;', + 'stroke: none;', + '}', + selector + ' .blocklyNonEditableText>text,', + selector + ' .blocklyEditableText>text {', + 'fill: #000;', + '}', + + // Editable field hover. + selector + ' .blocklyEditableText:not(.editing):hover>rect {', + 'stroke: #fff;', + 'stroke-width: 2;', + '}', + + // Text field input. + selector + ' .blocklyHtmlInput {', + 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', + 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', + '}', + + // Selection highlight. + selector + ' .blocklySelected>.blocklyPath {', + 'stroke: #fc3;', + 'stroke-width: 3px;', + '}', + + // Connection highlight. + selector + ' .blocklyHighlightedConnectionPath {', + 'stroke: #fc3;', + '}', + + // Replacable highlight. + selector + ' .blocklyReplaceable .blocklyPath {', + 'fill-opacity: .5;', + '}', + selector + ' .blocklyReplaceable .blocklyPathLight,', + selector + ' .blocklyReplaceable .blocklyPathDark {', + 'display: none;', + '}', + /* eslint-enable indent */ + ]; +}; diff --git a/core/renderers/common/debugger.js b/core/renderers/common/debugger.js index 023405588..2bb37a6c6 100644 --- a/core/renderers/common/debugger.js +++ b/core/renderers/common/debugger.js @@ -42,6 +42,7 @@ Blockly.blockRendering.Debug = function() { /** * An array of SVG elements that have been created by this object. * @type {Array.} + * @private */ this.debugElements_ = []; @@ -49,6 +50,7 @@ Blockly.blockRendering.Debug = function() { * The SVG root of the block that is being rendered. Debug elements will * be attached to this root. * @type {SVGElement} + * @private */ this.svgRoot_ = null; }; @@ -92,14 +94,20 @@ Blockly.blockRendering.Debug.prototype.drawSpacerRow = function(row, cursorY, is return; } + var height = Math.abs(row.height); + var isNegativeSpacing = row.height < 0; + if (isNegativeSpacing) { + cursorY -= height; + } + this.debugElements_.push(Blockly.utils.dom.createSvgElement('rect', { 'class': 'rowSpacerRect blockRenderDebug', 'x': isRtl ? -(row.xPos + row.width) : row.xPos, 'y': cursorY, 'width': row.width, - 'height': row.height, - 'stroke': 'blue', + 'height': height, + 'stroke': isNegativeSpacing ? 'black' : 'blue', 'fill': 'blue', 'fill-opacity': '0.5', 'stroke-width': '1px' @@ -119,9 +127,11 @@ Blockly.blockRendering.Debug.prototype.drawSpacerElem = function(elem, rowHeight return; } - var xPos = elem.xPos; + var width = Math.abs(elem.width); + var isNegativeSpacing = elem.width < 0; + var xPos = isNegativeSpacing ? elem.xPos - width : elem.xPos; if (isRtl) { - xPos = -(xPos + elem.width); + xPos = -(xPos + width); } var yPos = elem.centerline - elem.height / 2; this.debugElements_.push(Blockly.utils.dom.createSvgElement('rect', @@ -129,10 +139,10 @@ Blockly.blockRendering.Debug.prototype.drawSpacerElem = function(elem, rowHeight 'class': 'elemSpacerRect blockRenderDebug', 'x': xPos, 'y': yPos, - 'width': elem.width, + 'width': width, 'height': elem.height, 'stroke': 'pink', - 'fill': 'pink', + 'fill': isNegativeSpacing ? 'black' : 'pink', 'fill-opacity': '0.5', 'stroke-width': '1px' }, @@ -169,15 +179,17 @@ Blockly.blockRendering.Debug.prototype.drawRenderedElem = function(elem, isRtl) if (Blockly.blockRendering.Types.isInput(elem) && Blockly.blockRendering.Debug.config.connections) { - this.drawConnection(elem.connection); + this.drawConnection(elem.connectionModel); } }; /** * Draw a circle at the location of the given connection. Inputs and outputs - * share the same colors, as do previous and next. When positioned correctly + * share the same colours, as do previous and next. When positioned correctly * a connected pair will look like a bullseye. * @param {Blockly.RenderedConnection} conn The connection to circle. + * @suppress {visibility} Suppress visibility of conn.offsetInBlock_ since this + * is a debug module. * @package */ Blockly.blockRendering.Debug.prototype.drawConnection = function(conn) { @@ -270,10 +282,15 @@ Blockly.blockRendering.Debug.prototype.drawRenderedRow = function(row, cursorY, * @package */ Blockly.blockRendering.Debug.prototype.drawRowWithElements = function(row, cursorY, isRtl) { - for (var i = 0, elem; (elem = row.elements[i]); i++) { + for (var i = 0, l = row.elements.length; i < l; i++) { + var elem = row.elements[i]; + if (!elem) { + console.warn('A row has an undefined or null element.', row, elem); + continue; + } if (Blockly.blockRendering.Types.isSpacer(elem)) { this.drawSpacerElem( - /** @type {Blockly.blockRendering.InRowSpacer} */ (elem), + /** @type {!Blockly.blockRendering.InRowSpacer} */ (elem), row.height, isRtl); } else { this.drawRenderedElem(elem, isRtl); @@ -360,6 +377,9 @@ Blockly.blockRendering.Debug.prototype.drawDebug = function(block, info) { if (block.outputConnection) { this.drawConnection(block.outputConnection); } + if (info.rightSide) { + this.drawRenderedElem(info.rightSide, info.RTL); + } this.drawBoundingBox(info); }; diff --git a/core/renderers/common/drawer.js b/core/renderers/common/drawer.js index 5887fa767..b498bba1f 100644 --- a/core/renderers/common/drawer.js +++ b/core/renderers/common/drawer.js @@ -72,7 +72,7 @@ Blockly.blockRendering.Drawer.prototype.draw = function() { this.drawOutline_(); this.drawInternals_(); - this.block_.pathObject.setPaths(this.outlinePath_ + '\n' + this.inlinePath_); + this.block_.pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_); if (this.info_.RTL) { this.block_.pathObject.flipRTL(); } @@ -144,6 +144,9 @@ Blockly.blockRendering.Drawer.prototype.drawTop_ = function() { if (Blockly.blockRendering.Types.isLeftRoundedCorner(elem)) { this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft; + } else if (Blockly.blockRendering.Types.isRightRoundedCorner(elem)) { + this.outlinePath_ += + this.constants_.OUTSIDE_CORNERS.topRight; } else if (Blockly.blockRendering.Types.isPreviousConnection(elem)) { this.outlinePath_ += elem.shape.pathLeft; } else if (Blockly.blockRendering.Types.isHat(elem)) { @@ -242,20 +245,26 @@ Blockly.blockRendering.Drawer.prototype.drawBottom_ = function() { var elems = bottomRow.elements; this.positionNextConnection_(); - this.outlinePath_ += - Blockly.utils.svgPaths.lineOnAxis('V', bottomRow.baseline); - + var rightCornerYOffset = 0; + var outlinePath = ''; for (var i = elems.length - 1, elem; (elem = elems[i]); i--) { if (Blockly.blockRendering.Types.isNextConnection(elem)) { - this.outlinePath_ += elem.shape.pathRight; + outlinePath += elem.shape.pathRight; } else if (Blockly.blockRendering.Types.isLeftSquareCorner(elem)) { - this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('H', bottomRow.xPos); + outlinePath += Blockly.utils.svgPaths.lineOnAxis('H', bottomRow.xPos); } else if (Blockly.blockRendering.Types.isLeftRoundedCorner(elem)) { - this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.bottomLeft; + outlinePath += this.constants_.OUTSIDE_CORNERS.bottomLeft; + } else if (Blockly.blockRendering.Types.isRightRoundedCorner(elem)) { + outlinePath += this.constants_.OUTSIDE_CORNERS.bottomRight; + rightCornerYOffset = this.constants_.OUTSIDE_CORNERS.rightHeight; } else if (Blockly.blockRendering.Types.isSpacer(elem)) { - this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('h', elem.width * -1); + outlinePath += Blockly.utils.svgPaths.lineOnAxis('h', elem.width * -1); } } + + this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('V', + bottomRow.baseline - rightCornerYOffset); + this.outlinePath_ += outlinePath; }; /** @@ -381,13 +390,14 @@ Blockly.blockRendering.Drawer.prototype.drawInlineInput_ = function(input) { Blockly.blockRendering.Drawer.prototype.positionInlineInputConnection_ = function(input) { var yPos = input.centerline - input.height / 2; // Move the connection. - if (input.connection) { + if (input.connectionModel) { // xPos already contains info about startX - var connX = input.xPos + input.connectionWidth; + var connX = input.xPos + input.connectionWidth + input.connectionOffsetX; if (this.info_.RTL) { connX *= -1; } - input.connection.setOffsetInBlock(connX, yPos + input.connectionOffsetY); + input.connectionModel.setOffsetInBlock(connX, + yPos + input.connectionOffsetY); } }; @@ -400,12 +410,12 @@ Blockly.blockRendering.Drawer.prototype.positionInlineInputConnection_ = functio */ Blockly.blockRendering.Drawer.prototype.positionStatementInputConnection_ = function(row) { var input = row.getLastInput(); - if (input.connection) { + if (input.connectionModel) { var connX = row.xPos + row.statementEdge + input.notchOffset; if (this.info_.RTL) { connX *= -1; } - input.connection.setOffsetInBlock(connX, row.yPos); + input.connectionModel.setOffsetInBlock(connX, row.yPos); } }; @@ -418,12 +428,12 @@ Blockly.blockRendering.Drawer.prototype.positionStatementInputConnection_ = func */ Blockly.blockRendering.Drawer.prototype.positionExternalValueConnection_ = function(row) { var input = row.getLastInput(); - if (input.connection) { + if (input.connectionModel) { var connX = row.xPos + row.width; if (this.info_.RTL) { connX *= -1; } - input.connection.setOffsetInBlock(connX, row.yPos); + input.connectionModel.setOffsetInBlock(connX, row.yPos); } }; @@ -451,8 +461,7 @@ Blockly.blockRendering.Drawer.prototype.positionNextConnection_ = function() { var connInfo = bottomRow.connection; var x = connInfo.xPos; // Already contains info about startX var connX = (this.info_.RTL ? -x : x); - connInfo.connectionModel.setOffsetInBlock( - connX, (connInfo.centerline - connInfo.height / 2)); + connInfo.connectionModel.setOffsetInBlock(connX, bottomRow.baseline); } }; @@ -462,7 +471,7 @@ Blockly.blockRendering.Drawer.prototype.positionNextConnection_ = function() { */ Blockly.blockRendering.Drawer.prototype.positionOutputConnection_ = function() { if (this.info_.outputConnection) { - var x = this.info_.startX; + var x = this.info_.startX + this.info_.outputConnection.connectionOffsetX; var connX = this.info_.RTL ? -x : x; this.block_.outputConnection.setOffsetInBlock(connX, this.info_.outputConnection.connectionOffsetY); diff --git a/core/renderers/common/i_path_object.js b/core/renderers/common/i_path_object.js new file mode 100644 index 000000000..12454e41d --- /dev/null +++ b/core/renderers/common/i_path_object.js @@ -0,0 +1,130 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview The interface for an object that owns a block's rendering SVG + * elements. + * @author fenichel@google.com (Rachel Fenichel) + */ + +'use strict'; + +goog.provide('Blockly.blockRendering.IPathObject'); + +goog.requireType('Blockly.blockRendering.ConstantProvider'); +goog.requireType('Blockly.Theme'); + + +/** + * An interface for a block's path object. + * @param {!SVGElement} _root The root SVG element. + * @param {!Blockly.blockRendering.ConstantProvider} _constants The renderer's + * constants. + * @interface + */ +Blockly.blockRendering.IPathObject = function(_root, _constants) {}; + +/** + * Set the path generated by the renderer onto the respective SVG element. + * @param {string} pathString The path. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.setPath; + +/** + * Apply the stored colours to the block's path, taking into account whether + * the paths belong to a shadow block. + * @param {!Blockly.Block} block The source block. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.applyColour; + +/** + * Update the style. + * @param {!Blockly.Theme.BlockStyle} blockStyle The block style to use. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.setStyle; + +/** + * Flip the SVG paths in RTL. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.flipRTL; + +/** + * Add the cursor svg to this block's svg group. + * @param {SVGElement} cursorSvg The svg root of the cursor to be added to the + * block svg group. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.setCursorSvg; + +/** + * Add the marker svg to this block's svg group. + * @param {SVGElement} markerSvg The svg root of the marker to be added to the + * block svg group. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.setMarkerSvg; + +/** + * Set whether the block shows a highlight or not. Block highlighting is + * often used to visually mark blocks currently being executed. + * @param {boolean} highlighted True if highlighted. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.updateHighlighted; + +/** + * Add or remove styling showing that a block is selected. + * @param {boolean} enable True if selection is enabled, false otherwise. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.updateSelected; + +/** + * Add or remove styling showing that a block is dragged over a delete area. + * @param {boolean} enable True if the block is being dragged over a delete + * area, false otherwise. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.updateDraggingDelete; + +/** + * Add or remove styling showing that a block is an insertion marker. + * @param {boolean} enable True if the block is an insertion marker, false + * otherwise. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.updateInsertionMarker; + +/** + * Add or remove styling showing that a block is movable. + * @param {boolean} enable True if the block is movable, false otherwise. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.updateMovable; + +/** + * Add or remove styling that shows that if the dragging block is dropped, this + * block will be replaced. If a shadow block, it will disappear. Otherwise it + * will bump. + * @param {boolean} enable True if styling should be added. + * @package + */ +Blockly.blockRendering.IPathObject.prototype.updateReplacementHighlight; diff --git a/core/renderers/common/info.js b/core/renderers/common/info.js index fc3898b7a..ec63bce6c 100644 --- a/core/renderers/common/info.js +++ b/core/renderers/common/info.js @@ -138,6 +138,13 @@ Blockly.blockRendering.RenderInfo = function(renderer, block) { */ this.rows = []; + /** + * The total number of input rows added onto the block. + * @type {number} + * @protected + */ + this.inputRowNum_ = 1; + /** * An array of measurable objects containing hidden icons. * @type {!Array.} @@ -184,9 +191,9 @@ Blockly.blockRendering.RenderInfo.prototype.getRenderer = function() { Blockly.blockRendering.RenderInfo.prototype.measure = function() { this.createRows_(); this.addElemSpacing_(); + this.addRowSpacing_(); this.computeBounds_(); this.alignRowElements_(); - this.addRowSpacing_(); this.finalize_(); }; @@ -224,6 +231,7 @@ Blockly.blockRendering.RenderInfo.prototype.createRows_ = function() { // Finish this row and create a new one. this.rows.push(activeRow); activeRow = new Blockly.blockRendering.InputRow(this.constants_); + this.inputRowNum_ ++; } // All of the fields in an input go on the same row. @@ -254,8 +262,8 @@ Blockly.blockRendering.RenderInfo.prototype.createRows_ = function() { */ Blockly.blockRendering.RenderInfo.prototype.populateTopRow_ = function() { var hasPrevious = !!this.block_.previousConnection; - var hasHat = (this.block_.hat ? - this.block_.hat === 'cap' : Blockly.BlockSvg.START_HAT) && + var hasHat = (typeof this.block_.hat !== 'undefined' ? + this.block_.hat === 'cap' : this.constants_.ADD_START_HATS) && !this.outputConnection && !hasPrevious; var leftSquareCorner = this.topRow.hasLeftSquareCorner(this.block_); @@ -286,9 +294,20 @@ Blockly.blockRendering.RenderInfo.prototype.populateTopRow_ = function() { // This is the minimum height for the row. If one of its elements has a // greater height it will be overwritten in the compute pass. if (precedesStatement && !this.block_.isCollapsed()) { - this.topRow.minHeight = this.constants_.LARGE_PADDING; + this.topRow.minHeight = + this.constants_.TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT; } else { - this.topRow.minHeight = this.constants_.MEDIUM_PADDING; + this.topRow.minHeight = this.constants_.TOP_ROW_MIN_HEIGHT; + } + + var rightSquareCorner = this.topRow.hasRightSquareCorner(this.block_); + + if (rightSquareCorner) { + this.topRow.elements.push( + new Blockly.blockRendering.SquareCorner(this.constants_, 'right')); + } else { + this.topRow.elements.push( + new Blockly.blockRendering.RoundCorner(this.constants_, 'right')); } }; @@ -307,9 +326,10 @@ Blockly.blockRendering.RenderInfo.prototype.populateBottomRow_ = function() { // This is the minimum height for the row. If one of its elements has a // greater height it will be overwritten in the compute pass. if (followsStatement) { - this.bottomRow.minHeight = this.constants_.LARGE_PADDING; + this.bottomRow.minHeight = + this.constants_.BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT; } else { - this.bottomRow.minHeight = this.constants_.MEDIUM_PADDING - 1; + this.bottomRow.minHeight = this.constants_.BOTTOM_ROW_MIN_HEIGHT; } var leftSquareCorner = this.bottomRow.hasLeftSquareCorner(this.block_); @@ -328,6 +348,16 @@ Blockly.blockRendering.RenderInfo.prototype.populateBottomRow_ = function() { /** @type {Blockly.RenderedConnection} */ (this.block_.nextConnection)); this.bottomRow.elements.push(this.bottomRow.connection); } + + var rightSquareCorner = this.bottomRow.hasRightSquareCorner(this.block_); + + if (rightSquareCorner) { + this.bottomRow.elements.push( + new Blockly.blockRendering.SquareCorner(this.constants_, 'right')); + } else { + this.bottomRow.elements.push( + new Blockly.blockRendering.RoundCorner(this.constants_, 'right')); + } }; /** @@ -356,10 +386,14 @@ Blockly.blockRendering.RenderInfo.prototype.addInput_ = function(input, activeRo // Dummy inputs have no visual representation, but the information is still // important. activeRow.minHeight = Math.max(activeRow.minHeight, + input.getSourceBlock() && input.getSourceBlock().isShadow() ? + this.constants_.DUMMY_INPUT_SHADOW_MIN_HEIGHT : this.constants_.DUMMY_INPUT_MIN_HEIGHT); activeRow.hasDummyInput = true; } - activeRow.align = input.align; + if (activeRow.align == null) { + activeRow.align = input.align; + } }; /** @@ -401,6 +435,9 @@ Blockly.blockRendering.RenderInfo.prototype.addElemSpacing_ = function() { row.elements.push(new Blockly.blockRendering.InRowSpacer( this.constants_, this.getInRowSpacing_(null, oldElems[0]))); } + if (!oldElems.length) { + continue; + } for (var e = 0; e < oldElems.length - 1; e++) { row.elements.push(oldElems[e]); var spacing = this.getInRowSpacing_(oldElems[e], oldElems[e + 1]); @@ -428,6 +465,12 @@ Blockly.blockRendering.RenderInfo.prototype.addElemSpacing_ = function() { * @protected */ Blockly.blockRendering.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { + if (!prev) { + // Statement input padding. + if (next && Blockly.blockRendering.Types.isStatementInput(next)) { + return this.constants_.STATEMENT_INPUT_PADDING_LEFT; + } + } // Between inputs and the end of the row. if (prev && Blockly.blockRendering.Types.isInput(prev) && !next) { if (Blockly.blockRendering.Types.isExternalInput(prev)) { @@ -480,9 +523,7 @@ Blockly.blockRendering.RenderInfo.prototype.computeBounds_ = function() { Math.max(widestRowWithConnectedBlocks, row.widthWithConnectedBlocks); } - this.statementEdge = widestStatementRowFields; - this.width = blockWidth; for (var i = 0, row; (row = this.rows[i]); i++) { @@ -513,15 +554,26 @@ Blockly.blockRendering.RenderInfo.prototype.alignRowElements_ = function() { /** @type {!Blockly.blockRendering.InputRow} */ (row)); } else { var currentWidth = row.width; - var desiredWidth = this.width - this.startX; + var desiredWidth = this.getDesiredRowWidth_(row); var missingSpace = desiredWidth - currentWidth; - if (missingSpace) { + if (missingSpace > 0) { this.addAlignmentPadding_(row, missingSpace); } } } }; +/** + * Calculate the desired width of an input row. + * @param {!Blockly.blockRendering.Row} _row The input row. + * @return {number} The desired width of the input row. + * @protected + */ +Blockly.blockRendering.RenderInfo.prototype.getDesiredRowWidth_ = function( + _row) { + return this.width - this.startX; +}; + /** * Modify the given row to add the given amount of padding around its fields. * The exact location of the padding is based on the alignment property of the @@ -532,11 +584,28 @@ Blockly.blockRendering.RenderInfo.prototype.alignRowElements_ = function() { */ Blockly.blockRendering.RenderInfo.prototype.addAlignmentPadding_ = function(row, missingSpace) { + var firstSpacer = row.getFirstSpacer(); var lastSpacer = row.getLastSpacer(); - if (lastSpacer) { - lastSpacer.width += missingSpace; - row.width += missingSpace; + 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. + lastSpacer.width += missingSpace; + } else if (row.align == Blockly.ALIGN_CENTRE) { + // Split the padding between the beginning and end of the row. + firstSpacer.width += missingSpace / 2; + lastSpacer.width += missingSpace / 2; + } else if (row.align == Blockly.ALIGN_RIGHT) { + // Add padding at the beginning of the row. + firstSpacer.width += missingSpace; + } else { + // Default to left-aligning. + lastSpacer.width += missingSpace; + } + row.width += missingSpace; }; /** @@ -551,15 +620,15 @@ Blockly.blockRendering.RenderInfo.prototype.alignStatementRow_ = function(row) { var desiredWidth = this.statementEdge; // Add padding before the statement input. var missingSpace = desiredWidth - currentWidth; - if (missingSpace) { + if (missingSpace > 0) { this.addAlignmentPadding_(row, missingSpace); } // Also widen the statement input to reach to the right side of the // block. Note that this does not add padding. currentWidth = row.width; - var rightCornerWidth = this.constants_.INSIDE_CORNERS.rightWidth || 0; - desiredWidth = this.width - this.startX - rightCornerWidth; + desiredWidth = this.getDesiredRowWidth_(row); statementInput.width += (desiredWidth - currentWidth); + statementInput.height = Math.max(statementInput.height, row.height); row.width += (desiredWidth - currentWidth); row.widthWithConnectedBlocks = Math.max(row.width, this.statementEdge + row.connectedBlockWidths); @@ -596,6 +665,9 @@ Blockly.blockRendering.RenderInfo.prototype.makeSpacerRow_ = function(prev, next if (prev.hasStatement) { spacer.followsStatement = true; } + if (next.hasStatement) { + spacer.precedesStatement = true; + } return spacer; }; @@ -659,7 +731,7 @@ Blockly.blockRendering.RenderInfo.prototype.getElemCenterline_ = function(row, * Record final position information on elements on the given row, for use in * drawing. At minimum this records xPos and centerline on each element. * @param {!Blockly.blockRendering.Row} row The row containing the elements. - * @private + * @protected */ Blockly.blockRendering.RenderInfo.prototype.recordElemPositions_ = function( row) { @@ -695,8 +767,16 @@ Blockly.blockRendering.RenderInfo.prototype.finalize_ = function() { Math.max(widestRowWithConnectedBlocks, row.widthWithConnectedBlocks); this.recordElemPositions_(row); } + if (this.outputConnection && this.block_.nextConnection && + this.block_.nextConnection.isConnected()) { + // Include width of connected block in value to stack width measurement. + widestRowWithConnectedBlocks = + Math.max(widestRowWithConnectedBlocks, + this.block_.nextConnection.targetBlock().getHeightWidth().width); + } - this.widthWithChildren = widestRowWithConnectedBlocks + this.startX; + this.widthWithChildren = Math.max(this.widthWithChildren, + widestRowWithConnectedBlocks + this.startX); this.height = yCursor; this.startY = this.topRow.capline; diff --git a/core/renderers/common/marker_svg.js b/core/renderers/common/marker_svg.js new file mode 100644 index 000000000..f845922cc --- /dev/null +++ b/core/renderers/common/marker_svg.js @@ -0,0 +1,625 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a marker as SVG. + * @author samelh@microsoft.com (Sam El-Husseini) + */ +'use strict'; + + +goog.provide('Blockly.blockRendering.MarkerSvg'); + +goog.require('Blockly.ASTNode'); + + +/** + * Class for a marker. + * @param {!Blockly.WorkspaceSvg} workspace The workspace the marker belongs to. + * @param {!Blockly.blockRendering.ConstantProvider} constants The constants for + * the renderer. + * @param {!Blockly.Marker} marker The marker to draw. + * @constructor + */ +Blockly.blockRendering.MarkerSvg = function(workspace, constants, marker) { + /** + * The workspace the marker belongs to. + * @type {!Blockly.WorkspaceSvg} + * @private + */ + this.workspace_ = workspace; + + /** + * The marker to draw. + * @type {!Blockly.Marker} + * @private + */ + this.marker_ = marker; + + /** + * The workspace, field, or block that the marker SVG element should be + * attached to. + * @type {Blockly.WorkspaceSvg|Blockly.Field|Blockly.BlockSvg} + * @private + */ + this.parent_ = null; + + /** + * The constants necessary to draw the marker. + * @type {Blockly.blockRendering.ConstantProvider} + * @protected + */ + this.constants_ = constants; + + /** + * The current SVG element for the marker. + * @type {Element} + */ + this.currentMarkerSvg = null; + + var defaultColour = this.isCursor() ? this.constants_.CURSOR_COLOUR : + this.constants_.MARKER_COLOUR; + + /** + * The colour of the marker. + * @type {string} + */ + this.colour_ = marker.colour || defaultColour; +}; + +/** + * The name of the CSS class for a cursor. + * @const {string} + */ +Blockly.blockRendering.MarkerSvg.CURSOR_CLASS = 'blocklyCursor'; + +/** + * The name of the CSS class for a marker. + * @const {string} + */ +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 + */ +Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER = 3 / 4; + +/** + * Return the root node of the SVG or null if none exists. + * @return {SVGElement} The root SVG node. + */ +Blockly.blockRendering.MarkerSvg.prototype.getSvgRoot = function() { + return this.svgGroup_; +}; + +/** + * 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. + * @return {boolean} True if the marker is a cursor, false otherwise. + */ +Blockly.blockRendering.MarkerSvg.prototype.isCursor = function() { + return this.marker_.type == 'cursor'; +}; + +/** + * Create the DOM element for the marker. + * @return {!SVGElement} The marker controls SVG group. + * @package + */ +Blockly.blockRendering.MarkerSvg.prototype.createDom = function() { + var className = this.isCursor() ? + Blockly.blockRendering.MarkerSvg.CURSOR_CLASS : + Blockly.blockRendering.MarkerSvg.MARKER_CLASS; + + this.svgGroup_ = + Blockly.utils.dom.createSvgElement('g', { + 'class': className + }, null); + + this.createDomInternal_(); + 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. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.setParent_ = function(newParent) { + if (!this.isCursor()) { + if (this.parent_) { + this.parent_.setMarkerSvg(null); + } + newParent.setMarkerSvg(this.getSvgRoot()); + } else { + if (this.parent_) { + this.parent_.setCursorSvg(null); + } + newParent.setCursorSvg(this.getSvgRoot()); + } + this.parent_ = newParent; +}; + +/************************** + * Display + **************************/ + +/** + * 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 + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithBlockPrevOutput_ = function(block) { + if (!block) { + return; + } + var width = block.width; + var height = block.height; + var markerHeight = height * Blockly.blockRendering.MarkerSvg.HEIGHT_MULTIPLIER; + var markerOffset = this.constants_.CURSOR_BLOCK_PADDING; + + if (block.previousConnection) { + var connectionShape = this.constants_.shapeFor(block.previousConnection); + this.positionPrevious_(width, markerOffset, markerHeight, connectionShape); + } else if (block.outputConnection) { + var connectionShape = this.constants_.shapeFor(block.outputConnection); + this.positionOutput_(width, height, connectionShape); + } 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. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithCoordinates_ = function(curNode) { + var wsCoordinate = curNode.getWsCoordinate(); + var x = wsCoordinate.x; + var y = wsCoordinate.y; + + if (this.workspace_.RTL) { + x -= this.constants_.CURSOR_WS_WIDTH; + } + + this.positionLine_(x, y, this.constants_.CURSOR_WS_WIDTH); + this.setParent_(this.workspace_); + this.showCurrent_(); +}; + +/** + * Show the visual representation of a field. + * This is a box around the field. + * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithField_ = function(curNode) { + var field = /** @type {Blockly.Field} */ (curNode.getLocation()); + var width = field.getSize().width; + var height = field.getSize().height; + + this.positionRect_(0, 0, width, height); + this.setParent_(field); + this.showCurrent_(); +}; + +/** + * Show the visual representation of an input. + * This is a puzzle piece. + * @param {!Blockly.ASTNode} curNode The node that we want to draw the marker for. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithInput_ = function(curNode) { + var connection = /** @type {Blockly.RenderedConnection} */ + (curNode.getLocation()); + var sourceBlock = /** @type {!Blockly.BlockSvg} */ (connection.getSourceBlock()); + + this.positionInput_(connection); + this.setParent_(sourceBlock); + this.showCurrent_(); +}; + + +/** + * Show the visual representation of a next connection. + * This is a horizontal line. + * @param {!Blockly.ASTNode} curNode The node that we want 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 x = 0; + var y = connection.getOffsetInBlock().y; + var width = targetBlock.getHeightWidth().width; + if (this.workspace_.RTL) { + x = -width; + } + this.positionLine_(x, y, width); + this.setParent_(targetBlock); + this.showCurrent_(); +}; + +/** + * Show the visual representation of 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. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showWithStack_ = function(curNode) { + var block = /** @type {Blockly.BlockSvg} */ (curNode.getLocation()); + + // Gets the height and width of entire stack. + var heightWidth = block.getHeightWidth(); + + // Add padding so that being on a stack looks different than being on a block. + var width = heightWidth.width + this.constants_.CURSOR_STACK_PADDING; + var height = heightWidth.height + this.constants_.CURSOR_STACK_PADDING; + + // Shift the rectangle slightly to upper left so padding is equal on all sides. + var xPadding = -this.constants_.CURSOR_STACK_PADDING / 2; + var yPadding = -this.constants_.CURSOR_STACK_PADDING / 2; + + var x = xPadding; + var y = yPadding; + + if (this.workspace_.RTL) { + x = -(width + xPadding); + } + this.positionRect_(x, y, width, height); + this.setParent_(block); + this.showCurrent_(); +}; + +/** + * Show the current marker. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.showCurrent_ = function() { + this.hide(); + this.currentMarkerSvg.style.display = ''; +}; + +/************************** + * Position + **************************/ + +/** + * Position the marker for a block. + * Displays an outline of the top half of a rectangle around a block. + * @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 + */ +Blockly.blockRendering.MarkerSvg.prototype.positionBlock_ = function( + width, markerOffset, markerHeight) { + var markerPath = Blockly.utils.svgPaths.moveBy(-markerOffset, markerHeight) + + Blockly.utils.svgPaths.lineOnAxis('V', -markerOffset) + + Blockly.utils.svgPaths.lineOnAxis('H', width + markerOffset * 2) + + Blockly.utils.svgPaths.lineOnAxis('V', markerHeight); + this.markerBlock_.setAttribute('d', markerPath); + if (this.workspace_.RTL) { + this.flipRtl_(this.markerBlock_); + } + this.currentMarkerSvg = this.markerBlock_; +}; + +/** + * Position the marker for an input connection. + * Displays a filled in puzzle piece. + * @param {!Blockly.RenderedConnection} connection The connection to position marker around. + * @private + */ +Blockly.blockRendering.MarkerSvg.prototype.positionInput_ = function(connection) { + var x = connection.getOffsetInBlock().x; + var y = connection.getOffsetInBlock().y; + + var path = Blockly.utils.svgPaths.moveTo(0, 0) + + this.constants_.shapeFor(connection).pathDown; + + this.markerInput_.setAttribute('d', path); + this.markerInput_.setAttribute('transform', + 'translate(' + x + ',' + y + ')' + (this.workspace_.RTL ? ' scale(-1 1)' : '')); + this.currentMarkerSvg = this.markerInput_; +}; + +/** + * Move and show the marker at the specified coordinate in workspace units. + * Displays a horizontal line. + * @param {number} x The new x, in workspace units. + * @param {number} y The new y, in workspace units. + * @param {number} width The new width, in workspace units. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.positionLine_ = function(x, y, width) { + this.markerSvgLine_.setAttribute('x', x); + this.markerSvgLine_.setAttribute('y', y); + this.markerSvgLine_.setAttribute('width', width); + this.currentMarkerSvg = this.markerSvgLine_; +}; + +/** + * Position the marker for an output connection. + * Displays a puzzle outline and the top and bottom path. + * @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 + */ +Blockly.blockRendering.MarkerSvg.prototype.positionOutput_ = function( + width, height, connectionShape) { + var markerPath = Blockly.utils.svgPaths.moveBy(width, 0) + + Blockly.utils.svgPaths.lineOnAxis( + 'h', -(width - connectionShape.width)) + + Blockly.utils.svgPaths.lineOnAxis( + 'v', this.constants_.TAB_OFFSET_FROM_TOP) + + connectionShape.pathDown + + Blockly.utils.svgPaths.lineOnAxis('V', height) + + Blockly.utils.svgPaths.lineOnAxis('H', width); + this.markerBlock_.setAttribute('d', markerPath); + if (this.workspace_.RTL) { + this.flipRtl_(this.markerBlock_); + } + this.currentMarkerSvg = this.markerBlock_; +}; + +/** + * Position the marker for a previous connection. + * Displays a half rectangle with a notch in the top to represent the previous + * connection. + * @param {number} width The width of the block. + * @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 + */ +Blockly.blockRendering.MarkerSvg.prototype.positionPrevious_ = function( + width, markerOffset, markerHeight, connectionShape) { + var markerPath = Blockly.utils.svgPaths.moveBy(-markerOffset, markerHeight) + + Blockly.utils.svgPaths.lineOnAxis('V', -markerOffset) + + Blockly.utils.svgPaths.lineOnAxis( + 'H', this.constants_.NOTCH_OFFSET_LEFT) + + connectionShape.pathLeft + + Blockly.utils.svgPaths.lineOnAxis( + 'H', width + markerOffset * 2) + + Blockly.utils.svgPaths.lineOnAxis('V', markerHeight); + this.markerBlock_.setAttribute('d', markerPath); + if (this.workspace_.RTL) { + this.flipRtl_(this.markerBlock_); + } + this.currentMarkerSvg = this.markerBlock_; +}; + +/** + * Move and show the marker at the specified coordinate in workspace units. + * Displays a filled in rectangle. + * @param {number} x The new x, in workspace units. + * @param {number} y The new y, in workspace units. + * @param {number} width The new width, in workspace units. + * @param {number} height The new height, in workspace units. + * @protected + */ +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); + this.markerSvgRect_.setAttribute('height', height); + this.currentMarkerSvg = this.markerSvgRect_; +}; + +/** + * Flip the SVG paths in RTL. + * @param {!SVGElement} markerSvg The marker that we want to flip. + * @private + */ +Blockly.blockRendering.MarkerSvg.prototype.flipRtl_ = function(markerSvg) { + markerSvg.setAttribute('transform', 'scale(-1 1)'); +}; + +/** + * Hide the marker. + * @package + */ +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'; +}; + +/** + * 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.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. + * @param {Blockly.ASTNode} oldNode The old node the marker used to be on. + * @param {!Blockly.ASTNode} curNode The new node the marker is currently on. + * @private + */ +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; + } + Blockly.Events.fire(event); +}; + +/** + * Get the properties to make a marker blink. + * @return {!Object} The object holding attributes to make the marker blink. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.getBlinkProperties_ = function() { + return { + 'attributeType': 'XML', + 'attributeName': 'fill', + 'dur': '1s', + 'values': this.colour_ + ';transparent;transparent;', + 'repeatCount': 'indefinite' + }; +}; + + +/** + * Create the marker SVG. + * @return {Element} The SVG node created. + * @protected + */ +Blockly.blockRendering.MarkerSvg.prototype.createDomInternal_ = function() { + /* This markup will be generated and added to the .svgGroup_: + + + + + + */ + + this.markerSvg_ = Blockly.utils.dom.createSvgElement('g', + { + 'width': this.constants_.CURSOR_WS_WIDTH, + 'height': this.constants_.WS_CURSOR_HEIGHT + }, this.svgGroup_); + + // A horizontal line used to represent a workspace coordinate or next connection. + this.markerSvgLine_ = Blockly.utils.dom.createSvgElement('rect', + { + 'fill': this.colour_, + 'width': this.constants_.CURSOR_WS_WIDTH, + 'height': this.constants_.WS_CURSOR_HEIGHT, + 'style': 'display: none' + }, + this.markerSvg_); + + // A filled in rectangle used to represent a stack. + this.markerSvgRect_ = Blockly.utils.dom.createSvgElement('rect', + { + 'class': 'blocklyVerticalMarker', + 'rx': 10, 'ry': 10, + 'style': 'display: none', + 'stroke': this.colour_ + }, + this.markerSvg_); + + // A filled in puzzle piece used to represent an input value. + this.markerInput_ = Blockly.utils.dom.createSvgElement('path', + { + 'transform': '', + 'style': 'display: none', + 'fill': this.colour_ + }, + this.markerSvg_); + + // A path used to represent a previous connection and a block, an output + // connection and a block, or a block. + this.markerBlock_ = Blockly.utils.dom.createSvgElement('path', + { + 'transform': '', + 'style': 'display: none', + 'fill': 'none', + 'stroke': this.colour_, + 'stroke-width': this.constants_.CURSOR_STROKE_WIDTH + }, + this.markerSvg_); + + // Markers and stack markers don't blink. + if (this.isCursor()) { + var blinkProperties = this.getBlinkProperties_(); + Blockly.utils.dom.createSvgElement('animate', this.getBlinkProperties_(), + this.markerSvgLine_); + Blockly.utils.dom.createSvgElement('animate', blinkProperties, + this.markerInput_); + blinkProperties['attributeName'] = 'stroke'; + Blockly.utils.dom.createSvgElement('animate', blinkProperties, + this.markerBlock_); + } + + return this.markerSvg_; +}; + +/** + * Dispose of this marker. + * @package + */ +Blockly.blockRendering.MarkerSvg.prototype.dispose = function() { + if (this.svgGroup_) { + Blockly.utils.dom.removeNode(this.svgGroup_); + } +}; diff --git a/core/renderers/common/path_object.js b/core/renderers/common/path_object.js index 81370fcdf..c52996638 100644 --- a/core/renderers/common/path_object.js +++ b/core/renderers/common/path_object.js @@ -22,29 +22,34 @@ 'use strict'; -goog.provide('Blockly.blockRendering.IPathObject'); goog.provide('Blockly.blockRendering.PathObject'); +goog.require('Blockly.blockRendering.ConstantProvider'); +goog.require('Blockly.blockRendering.IPathObject'); +goog.require('Blockly.Theme'); goog.require('Blockly.utils.dom'); -/** - * An interface for a block's path object. - * @param {!SVGElement} _root The root SVG element. - * @interface - * @package - */ -Blockly.blockRendering.IPathObject = function(_root) {}; - /** * An object that handles creating and setting each of the SVG elements * used by the renderer. * @param {!SVGElement} root The root SVG element. + * @param {!Blockly.Theme.BlockStyle} style The style object to use for + * colouring. + * @param {!Blockly.blockRendering.ConstantProvider} constants The renderer's + * constants. * @constructor * @implements {Blockly.blockRendering.IPathObject} * @package */ -Blockly.blockRendering.PathObject = function(root) { +Blockly.blockRendering.PathObject = function(root, style, constants) { + /** + * The renderer's constant provider. + * @type {!Blockly.blockRendering.ConstantProvider} + * @protected + */ + this.constants_ = constants; + this.svgRoot = root; /** @@ -55,26 +60,28 @@ Blockly.blockRendering.PathObject = function(root) { this.svgPath = Blockly.utils.dom.createSvgElement('path', {'class': 'blocklyPath'}, this.svgRoot); - // The light and dark paths need to exist (for now) because there is colouring - // code in block_svg that depends on them. But we will always set them to - // display: none, and eventually we want to remove them entirely. - /** - * The light path of the block. - * @type {SVGElement} + * The style object to use when colouring block paths. + * @type {!Blockly.Theme.BlockStyle} * @package */ - this.svgPathLight = Blockly.utils.dom.createSvgElement('path', - {'class': 'blocklyPathLight'}, this.svgRoot); + this.style = style; /** - * The dark path of the block. + * 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} - * @package + * @private */ - this.svgPathDark = Blockly.utils.dom.createSvgElement('path', - {'class': 'blocklyPathDark', 'transform': 'translate(1,1)'}, - this.svgRoot); + this.cursorSvg_ = null; + + /** + * 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} + * @private + */ + this.markerSvg_ = null; }; /** @@ -82,10 +89,8 @@ Blockly.blockRendering.PathObject = function(root) { * @param {string} pathString The path. * @package */ -Blockly.blockRendering.PathObject.prototype.setPaths = function(pathString) { +Blockly.blockRendering.PathObject.prototype.setPath = function(pathString) { this.svgPath.setAttribute('d', pathString); - this.svgPathLight.style.display = 'none'; - this.svgPathDark.style.display = 'none'; }; /** @@ -96,3 +101,188 @@ Blockly.blockRendering.PathObject.prototype.flipRTL = function() { // Mirror the block's path. this.svgPath.setAttribute('transform', 'scale(-1 1)'); }; + +/** + * Add the cursor svg to this block's svg group. + * @param {SVGElement} cursorSvg The svg root of the cursor to be added to the + * block svg group. + * @package + */ +Blockly.blockRendering.PathObject.prototype.setCursorSvg = function(cursorSvg) { + if (!cursorSvg) { + this.cursorSvg_ = null; + return; + } + + this.svgRoot.appendChild(cursorSvg); + this.cursorSvg_ = cursorSvg; +}; + +/** + * Add the marker svg to this block's svg group. + * @param {SVGElement} markerSvg The svg root of the marker to be added to the + * block svg group. + * @package + */ +Blockly.blockRendering.PathObject.prototype.setMarkerSvg = function(markerSvg) { + if (!markerSvg) { + this.markerSvg_ = null; + return; + } + + if (this.cursorSvg_) { + this.svgRoot.insertBefore(markerSvg, this.cursorSvg_); + } else { + this.svgRoot.appendChild(markerSvg); + } + this.markerSvg_ = markerSvg; +}; + +/** + * Apply the stored colours to the block's path, taking into account whether + * the paths belong to a shadow block. + * @param {!Blockly.Block} block The source block. + * @package + */ +Blockly.blockRendering.PathObject.prototype.applyColour = function(block) { + this.svgPath.setAttribute('stroke', this.style.colourTertiary); + this.svgPath.setAttribute('fill', this.style.colourPrimary); + + this.updateShadow_(block.isShadow()); + this.updateDisabled_(!block.isEnabled() || block.getInheritedDisabled()); +}; + +/** + * Set the style. + * @param {!Blockly.Theme.BlockStyle} blockStyle The block style to use. + * @package + */ +Blockly.blockRendering.PathObject.prototype.setStyle = function(blockStyle) { + this.style = blockStyle; +}; + +/** + * Add or remove the given CSS class on the path object's root SVG element. + * @param {string} className The name of the class to add or remove + * @param {boolean} add True if the class should be added. False if it should + * be removed. + * @protected + */ +Blockly.blockRendering.PathObject.prototype.setClass_ = function( + className, add) { + if (add) { + Blockly.utils.dom.addClass(/** @type {!Element} */ (this.svgRoot), + className); + } else { + Blockly.utils.dom.removeClass(/** @type {!Element} */ (this.svgRoot), + className); + } +}; + +/** + * Set whether the block shows a highlight or not. Block highlighting is + * often used to visually mark blocks currently being executed. + * @param {boolean} enable True if highlighted. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateHighlighted = function( + enable) { + if (enable) { + this.svgPath.setAttribute('filter', + 'url(#' + this.constants_.embossFilterId + ')'); + } else { + this.svgPath.setAttribute('filter', 'none'); + } +}; + +/** + * Updates the look of the block to reflect a shadow state. + * @param {boolean} shadow True if the block is a shadow block. + * @protected + */ +Blockly.blockRendering.PathObject.prototype.updateShadow_ = function(shadow) { + if (shadow) { + this.svgPath.setAttribute('stroke', 'none'); + this.svgPath.setAttribute('fill', this.style.colourSecondary); + } +}; + +/** + * Updates the look of the block to reflect a disabled state. + * @param {boolean} disabled True if disabled. + * @protected + */ +Blockly.blockRendering.PathObject.prototype.updateDisabled_ = function( + disabled) { + this.setClass_('blocklyDisabled', disabled); + if (disabled) { + this.svgPath.setAttribute('fill', + 'url(#' + this.constants_.disabledPatternId + ')'); + } +}; + +/** + * Add or remove styling showing that a block is selected. + * @param {boolean} enable True if selection is enabled, false otherwise. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateSelected = function(enable) { + this.setClass_('blocklySelected', enable); +}; + +/** + * Add or remove styling showing that a block is dragged over a delete area. + * @param {boolean} enable True if the block is being dragged over a delete + * area, false otherwise. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateDraggingDelete = function( + enable) { + this.setClass_('blocklyDraggingDelete', enable); +}; + +/** + * Add or remove styling showing that a block is an insertion marker. + * @param {boolean} enable True if the block is an insertion marker, false + * otherwise. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateInsertionMarker = function( + enable) { + this.setClass_('blocklyInsertionMarker', enable); +}; + +/** + * Add or remove styling showing that a block is movable. + * @param {boolean} enable True if the block is movable, false otherwise. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateMovable = function(enable) { + this.setClass_('blocklyDraggable', enable); +}; + +/** + * Add or remove styling that shows that if the dragging block is dropped, this + * block will be replaced. If a shadow block, it will disappear. Otherwise it + * will bump. + * @param {boolean} enable True if styling should be added. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateReplacementHighlight = + function(enable) { + /* eslint-disable indent */ + this.setClass_('blocklyReplaceable', enable); +}; /* eslint-enable indent */ + +/** + * Add or remove styling that shows that if the dragging block is dropped, this + * block will be connected to the input. + * @param {Blockly.Connection} _conn The connection on the input to highlight. + * @param {boolean} _enable True if styling should be added. + * @package + */ +Blockly.blockRendering.PathObject.prototype.updateShapeForInputHighlight = + function(_conn, _enable) { + /* eslint-disable indent */ + // NOP +}; /* eslint-enable indent */ diff --git a/core/renderers/common/renderer.js b/core/renderers/common/renderer.js index bd16cef4d..3ec261e2a 100644 --- a/core/renderers/common/renderer.js +++ b/core/renderers/common/renderer.js @@ -24,19 +24,29 @@ goog.provide('Blockly.blockRendering.Renderer'); goog.require('Blockly.blockRendering.ConstantProvider'); +goog.require('Blockly.blockRendering.MarkerSvg'); goog.require('Blockly.blockRendering.Drawer'); goog.require('Blockly.blockRendering.IPathObject'); goog.require('Blockly.blockRendering.PathObject'); goog.require('Blockly.blockRendering.RenderInfo'); -goog.require('Blockly.CursorSvg'); + +goog.requireType('Blockly.blockRendering.Debug'); /** * The base class for a block renderer. + * @param {string} name The renderer name. * @package * @constructor */ -Blockly.blockRendering.Renderer = function() { +Blockly.blockRendering.Renderer = function(name) { + + /** + * The renderer name. + * @type {string} + * @package + */ + this.name = name; /** * The renderer's constant provider. @@ -89,6 +99,7 @@ Blockly.blockRendering.Renderer.prototype.makeDrawer_ = function(block, info) { /** * Create a new instance of the renderer's debugger. * @return {!Blockly.blockRendering.Debug} The renderer debugger. + * @suppress {strictModuleDepCheck} Debug renderer only included in playground. * @protected */ Blockly.blockRendering.Renderer.prototype.makeDebugger_ = function() { @@ -99,27 +110,31 @@ Blockly.blockRendering.Renderer.prototype.makeDebugger_ = function() { }; /** - * Create a new instance of the renderer's cursor drawer. - * @param {!Blockly.WorkspaceSvg} workspace The workspace the cursor belongs to. - * @param {boolean=} opt_marker True if the cursor is a marker. A marker is used - * to save a location and is an immovable cursor. False or undefined if the - * cursor is not a marker. - * @return {!Blockly.CursorSvg} The cursor drawer. + * Create a new instance of the renderer's marker drawer. + * @param {!Blockly.WorkspaceSvg} workspace The workspace the marker belongs to. + * @param {!Blockly.Marker} marker The marker. + * @return {!Blockly.blockRendering.MarkerSvg} The object in charge of drawing + * the marker. * @package */ -Blockly.blockRendering.Renderer.prototype.makeCursorDrawer = function( - workspace, opt_marker) { - return new Blockly.CursorSvg(workspace, opt_marker); +Blockly.blockRendering.Renderer.prototype.makeMarkerDrawer = function( + workspace, marker) { + return new Blockly.blockRendering.MarkerSvg(workspace, this.getConstants(), marker); }; /** * Create a new instance of a renderer path object. * @param {!SVGElement} root The root SVG element. + * @param {!Blockly.Theme.BlockStyle} style The style object to use for + * colouring. * @return {!Blockly.blockRendering.IPathObject} The renderer path object. * @package */ -Blockly.blockRendering.Renderer.prototype.makePathObject = function(root) { - return new Blockly.blockRendering.PathObject(root); +Blockly.blockRendering.Renderer.prototype.makePathObject = function(root, + style) { + return new Blockly.blockRendering.PathObject(root, style, + /** @type {!Blockly.blockRendering.ConstantProvider} */ (this.constants_)); + }; /** @@ -134,6 +149,34 @@ Blockly.blockRendering.Renderer.prototype.getConstants = function() { (this.constants_)); }; +/** + * Determine whether or not to highlight a connection. + * @param {Blockly.Connection} _conn The connection to determine whether or not + * to highlight. + * @return {boolean} True if we should highlight the connection. + * @package + */ +Blockly.blockRendering.Renderer.prototype.shouldHighlightConnection = + function(_conn) { + /* eslint-disable indent */ + return true; +}; /* eslint-enable indent */ + +/** + * Determine whether or not to insert a dragged block into a stack. + * @param {!Blockly.Block} block The target block. + * @param {!Blockly.Connection} conn The closest connection. + * @return {boolean} True if we should insert the dragged block into the stack. + * @package + */ +Blockly.blockRendering.Renderer.prototype.shouldInsertDraggedBlock = + function(block, conn) { + /* eslint-disable indent */ + return !conn.isConnected() || + !!Blockly.Connection.lastConnectionInRow(block, + conn.targetConnection.getSourceBlock()); +}; /* eslint-enable indent */ + /** * Render the block. * @param {!Blockly.BlockSvg} block The block to render. diff --git a/core/renderers/geras/constants.js b/core/renderers/geras/constants.js index fd20f14ba..2e4952731 100644 --- a/core/renderers/geras/constants.js +++ b/core/renderers/geras/constants.js @@ -37,9 +37,21 @@ goog.require('Blockly.utils.object'); Blockly.geras.ConstantProvider = function() { Blockly.geras.ConstantProvider.superClass_.constructor.call(this); + /** + * @override + */ + this.FIELD_TEXT_BASELINE_CENTER = false; + // The dark/shadow path in classic rendering is the same as the normal block // path, but translated down one and right one. this.DARK_PATH_OFFSET = 1; + + /** + * The maximum width of a bottom row that follows a statement input and has + * inputs inline. + * @type {number} + */ + this.MAX_BOTTOM_WIDTH = 30; }; Blockly.utils.object.inherits(Blockly.geras.ConstantProvider, Blockly.blockRendering.ConstantProvider); diff --git a/core/renderers/geras/drawer.js b/core/renderers/geras/drawer.js index 61b38826b..d879dfdd4 100644 --- a/core/renderers/geras/drawer.js +++ b/core/renderers/geras/drawer.js @@ -26,11 +26,12 @@ goog.provide('Blockly.geras.Drawer'); goog.require('Blockly.blockRendering.ConstantProvider'); goog.require('Blockly.blockRendering.Drawer'); goog.require('Blockly.geras.Highlighter'); -goog.require('Blockly.geras.PathObject'); goog.require('Blockly.geras.RenderInfo'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.svgPaths'); +goog.requireType('Blockly.geras.PathObject'); + /** * An object that draws a block based on the given rendering information. @@ -57,10 +58,12 @@ Blockly.geras.Drawer.prototype.draw = function() { this.drawOutline_(); this.drawInternals_(); - this.block_.pathObject.setPaths(this.outlinePath_ + '\n' + this.inlinePath_, - this.highlighter_.getPath()); + var pathObject = + /** @type {!Blockly.geras.PathObject} */ (this.block_.pathObject); + pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_); + pathObject.setHighlightPath(this.highlighter_.getPath()); if (this.info_.RTL) { - this.block_.pathObject.flipRTL(); + pathObject.flipRTL(); } if (Blockly.blockRendering.useDebugger) { this.block_.renderingDebugger.drawDebug(this.block_, this.info_); @@ -110,7 +113,10 @@ Blockly.geras.Drawer.prototype.drawStatementInput_ = function(row) { */ Blockly.geras.Drawer.prototype.drawRightSideRow_ = function(row) { this.highlighter_.drawRightSideRow(row); - Blockly.geras.Drawer.superClass_.drawRightSideRow_.call(this, row); + + this.outlinePath_ += + Blockly.utils.svgPaths.lineOnAxis('H', row.xPos + row.width) + + Blockly.utils.svgPaths.lineOnAxis('V', row.yPos + row.height); }; /** @@ -148,14 +154,14 @@ Blockly.geras.Drawer.prototype.drawInlineInput_ = function(input) { Blockly.geras.Drawer.prototype.positionInlineInputConnection_ = function(input) { var yPos = input.centerline - input.height / 2; // Move the connection. - if (input.connection) { + if (input.connectionModel) { // xPos already contains info about startX var connX = input.xPos + input.connectionWidth + this.constants_.DARK_PATH_OFFSET; if (this.info_.RTL) { connX *= -1; } - input.connection.setOffsetInBlock( + input.connectionModel.setOffsetInBlock( connX, yPos + input.connectionOffsetY + this.constants_.DARK_PATH_OFFSET); } @@ -166,14 +172,14 @@ Blockly.geras.Drawer.prototype.positionInlineInputConnection_ = function(input) */ Blockly.geras.Drawer.prototype.positionStatementInputConnection_ = function(row) { var input = row.getLastInput(); - if (input.connection) { + if (input.connectionModel) { var connX = row.xPos + row.statementEdge + input.notchOffset; if (this.info_.RTL) { connX *= -1; } else { connX += this.constants_.DARK_PATH_OFFSET; } - input.connection.setOffsetInBlock(connX, + input.connectionModel.setOffsetInBlock(connX, row.yPos + this.constants_.DARK_PATH_OFFSET); } }; @@ -183,13 +189,13 @@ Blockly.geras.Drawer.prototype.positionStatementInputConnection_ = function(row) */ Blockly.geras.Drawer.prototype.positionExternalValueConnection_ = function(row) { var input = row.getLastInput(); - if (input.connection) { + if (input.connectionModel) { var connX = row.xPos + row.width + this.constants_.DARK_PATH_OFFSET; if (this.info_.RTL) { connX *= -1; } - input.connection.setOffsetInBlock(connX, row.yPos); + input.connectionModel.setOffsetInBlock(connX, row.yPos); } }; @@ -205,7 +211,6 @@ Blockly.geras.Drawer.prototype.positionNextConnection_ = function() { var connX = (this.info_.RTL ? -x : x) + (this.constants_.DARK_PATH_OFFSET / 2); connInfo.connectionModel.setOffsetInBlock( - connX, (connInfo.centerline - connInfo.height / 2) + - this.constants_.DARK_PATH_OFFSET); + connX, bottomRow.baseline + this.constants_.DARK_PATH_OFFSET); } }; diff --git a/core/renderers/geras/highlighter.js b/core/renderers/geras/highlighter.js index 7a49b6f4d..a740fe1cf 100644 --- a/core/renderers/geras/highlighter.js +++ b/core/renderers/geras/highlighter.js @@ -237,7 +237,6 @@ Blockly.geras.Highlighter.prototype.drawInlineInput = function(input) { var startY = yPos + offset; if (this.RTL_) { - // TODO: Check if this is different when the inline input is populated. var aboveTabHeight = input.connectionOffsetY - offset; var belowTabHeight = input.height - (input.connectionOffsetY + input.connectionHeight) + offset; diff --git a/core/renderers/geras/info.js b/core/renderers/geras/info.js index d072c24f5..7fe68853d 100644 --- a/core/renderers/geras/info.js +++ b/core/renderers/geras/info.js @@ -73,6 +73,28 @@ Blockly.geras.RenderInfo.prototype.getRenderer = function() { return /** @type {!Blockly.geras.Renderer} */ (this.renderer_); }; +/** + * @override + */ +Blockly.geras.RenderInfo.prototype.populateBottomRow_ = function() { + Blockly.geras.RenderInfo.superClass_.populateBottomRow_.call(this); + + var followsStatement = + this.block_.inputList.length && + this.block_.inputList[this.block_.inputList.length - 1] + .type == Blockly.NEXT_STATEMENT; + + // The minimum height of the bottom row is smaller in Geras than in other + // renderers, because the dark path adds a pixel. + // If one of the row's elements has a greater height this will be overwritten + // in the compute pass. + if (!followsStatement) { + this.bottomRow.minHeight = + this.constants_.MEDIUM_PADDING - this.constants_.DARK_PATH_OFFSET; + } + +}; + /** * @override */ @@ -97,7 +119,10 @@ Blockly.geras.RenderInfo.prototype.addInput_ = function(input, activeRow) { this.constants_.DUMMY_INPUT_MIN_HEIGHT); activeRow.hasDummyInput = true; } - activeRow.align = input.align; + // Ignore row alignment if inline. + if (!this.isInline && activeRow.align == null) { + activeRow.align = input.align; + } }; /** @@ -119,6 +144,9 @@ Blockly.geras.RenderInfo.prototype.addElemSpacing_ = function() { row.elements.push(new Blockly.blockRendering.InRowSpacer( this.constants_, this.getInRowSpacing_(null, oldElems[0]))); } + if (!oldElems.length) { + continue; + } for (var e = 0; e < oldElems.length - 1; e++) { row.elements.push(oldElems[e]); var spacing = this.getInRowSpacing_(oldElems[e], oldElems[e + 1]); @@ -144,7 +172,8 @@ Blockly.geras.RenderInfo.prototype.addElemSpacing_ = function() { Blockly.geras.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { if (!prev) { // Between an editable field and the beginning of the row. - if (next && Blockly.blockRendering.Types.isField(next) && next.isEditable) { + if (next && Blockly.blockRendering.Types.isField(next) && + (/** @type Blockly.blockRendering.Field */ (next)).isEditable) { return this.constants_.MEDIUM_PADDING; } // Inline input at the beginning of the row. @@ -158,10 +187,12 @@ Blockly.geras.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { return this.constants_.LARGE_PADDING; } - // Spacing between a non-input and the end of the row. - if (!Blockly.blockRendering.Types.isInput(prev) && !next) { + // Spacing between a non-input and the end of the row or a statement input. + if (!Blockly.blockRendering.Types.isInput(prev) && (!next || + Blockly.blockRendering.Types.isStatementInput(next))) { // Between an editable field and the end of the row. - if (Blockly.blockRendering.Types.isField(prev) && prev.isEditable) { + if (Blockly.blockRendering.Types.isField(prev) && + (/** @type Blockly.blockRendering.Field */ (prev)).isEditable) { return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -202,7 +233,8 @@ Blockly.geras.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { if (!Blockly.blockRendering.Types.isInput(prev) && next && Blockly.blockRendering.Types.isInput(next)) { // Between an editable field and an input. - if (prev.isEditable) { + if (Blockly.blockRendering.Types.isField(prev) && + (/** @type Blockly.blockRendering.Field */ (prev)).isEditable) { if (Blockly.blockRendering.Types.isInlineInput(next)) { return this.constants_.SMALL_PADDING; } else if (Blockly.blockRendering.Types.isExternalInput(next)) { @@ -228,9 +260,9 @@ Blockly.geras.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { // Spacing between an inline input and a field. if (Blockly.blockRendering.Types.isInlineInput(prev) && - next && !Blockly.blockRendering.Types.isInput(next)) { + next && Blockly.blockRendering.Types.isField(next)) { // Editable field after inline input. - if (next.isEditable) { + if ((/** @type Blockly.blockRendering.Field */ (next)).isEditable) { return this.constants_.MEDIUM_PADDING; } else { // Noneditable field after inline input. @@ -269,9 +301,10 @@ Blockly.geras.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { } // Spacing between two fields of the same editability. - if (!Blockly.blockRendering.Types.isInput(prev) && - next && !Blockly.blockRendering.Types.isInput(next) && - (prev.isEditable == next.isEditable)) { + if (Blockly.blockRendering.Types.isField(prev) && + next && Blockly.blockRendering.Types.isField(next) && + ((/** @type Blockly.blockRendering.Field */ (prev)).isEditable == + (/** @type Blockly.blockRendering.Field */ (next)).isEditable)) { return this.constants_.LARGE_PADDING; } @@ -283,34 +316,6 @@ Blockly.geras.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { return this.constants_.MEDIUM_PADDING; }; -/** - * @override - */ -Blockly.geras.RenderInfo.prototype.addAlignmentPadding_ = function(row, missingSpace) { - var firstSpacer = row.getFirstSpacer(); - var lastSpacer = row.getLastSpacer(); - 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. - lastSpacer.width += missingSpace; - } else if (row.align == Blockly.ALIGN_CENTRE) { - // Split the padding between the beginning and end of the row. - firstSpacer.width += missingSpace / 2; - lastSpacer.width += missingSpace / 2; - } else if (row.align == Blockly.ALIGN_RIGHT) { - // Add padding at the beginning of the row. - firstSpacer.width += missingSpace; - } else { - // Default to left-aligning. - lastSpacer.width += missingSpace; - } - row.width += missingSpace; -}; - /** * @override */ @@ -380,6 +385,68 @@ Blockly.geras.RenderInfo.prototype.getElemCenterline_ = function(row, elem) { return result; }; +/** + * @override + */ +Blockly.geras.RenderInfo.prototype.alignRowElements_ = function() { + if (!this.isInline) { + Blockly.geras.RenderInfo.superClass_.alignRowElements_.call(this); + return; + } + + // Walk backgrounds through rows on the block, keeping track of the right + // input edge. + var nextRightEdge = 0; + var prevInput = null; + for (var i = this.rows.length - 1, row; (row = this.rows[i]); i--) { + row.nextRightEdge = nextRightEdge; + if (Blockly.blockRendering.Types.isInputRow(row)) { + if (row.hasStatement) { + this.alignStatementRow_( + /** @type {!Blockly.blockRendering.InputRow} */ (row)); + } + if (prevInput && prevInput.hasStatement && row.width < prevInput.width) { + row.nextRightEdge = prevInput.width; + } else { + nextRightEdge = row.width; + } + prevInput = row; + } + } + // Walk down each row from the top, comparing the prev and next right input + // edges and setting the desired width to the max of the two. + var prevRightEdge = 0; + for (var i = 0, row; (row = this.rows[i]); i++) { + if (row.hasStatement) { + prevRightEdge = this.getDesiredRowWidth_(row); + } else if (Blockly.blockRendering.Types.isSpacer(row)) { + // Set the spacer row to the max of the prev or next input width. + row.width = Math.max(prevRightEdge, row.nextRightEdge); + } else { + var currentWidth = row.width; + var desiredWidth = Math.max(prevRightEdge, row.nextRightEdge); + var missingSpace = desiredWidth - currentWidth; + if (missingSpace > 0) { + this.addAlignmentPadding_(row, missingSpace); + } + prevRightEdge = row.width; + } + } +}; + +/** + * @override + */ +Blockly.geras.RenderInfo.prototype.getDesiredRowWidth_ = function( + row) { + // Limit the width of a statement row when a block is inline. + if (this.isInline && row.hasStatement) { + return this.statementEdge + this.constants_.MAX_BOTTOM_WIDTH + this.startX; + } + return Blockly.geras.RenderInfo.superClass_.getDesiredRowWidth_.call(this, + row); +}; + /** * @override */ @@ -407,6 +474,15 @@ Blockly.geras.RenderInfo.prototype.finalize_ = function() { } this.recordElemPositions_(row); } + if (this.outputConnection && this.block_.nextConnection && + this.block_.nextConnection.isConnected()) { + // Include width of connected block in value to stack width measurement. + widestRowWithConnectedBlocks = + Math.max(widestRowWithConnectedBlocks, + this.block_.nextConnection.targetBlock().getHeightWidth().width - + this.constants_.DARK_PATH_OFFSET); + } + this.bottomRow.baseline = yCursor - this.bottomRow.descenderHeight; // The dark (lowlight) adds to the size of the block in both x and y. diff --git a/core/renderers/geras/measurables/inputs.js b/core/renderers/geras/measurables/inputs.js index 441b7550f..f662b2969 100644 --- a/core/renderers/geras/measurables/inputs.js +++ b/core/renderers/geras/measurables/inputs.js @@ -25,9 +25,9 @@ goog.provide('Blockly.geras.InlineInput'); goog.provide('Blockly.geras.StatementInput'); -goog.require('Blockly.blockRendering.Connection'); goog.require('Blockly.utils.object'); + /** * An object containing information about the space an inline input takes up * during rendering @@ -37,7 +37,7 @@ goog.require('Blockly.utils.object'); * information for. * @package * @constructor - * @extends {Blockly.blockRendering.InputConnection} + * @extends {Blockly.blockRendering.InlineInput} */ Blockly.geras.InlineInput = function(constants, input) { Blockly.geras.InlineInput.superClass_.constructor.call( @@ -62,7 +62,7 @@ Blockly.utils.object.inherits(Blockly.geras.InlineInput, * information for. * @package * @constructor - * @extends {Blockly.blockRendering.InputConnection} + * @extends {Blockly.blockRendering.StatementInput} */ Blockly.geras.StatementInput = function(constants, input) { Blockly.geras.StatementInput.superClass_.constructor.call( diff --git a/core/renderers/geras/path_object.js b/core/renderers/geras/path_object.js index 416cb54a9..33143d729 100644 --- a/core/renderers/geras/path_object.js +++ b/core/renderers/geras/path_object.js @@ -24,19 +24,31 @@ goog.provide('Blockly.geras.PathObject'); -goog.require('Blockly.blockRendering.IPathObject'); +goog.require('Blockly.blockRendering.PathObject'); +goog.require('Blockly.geras.ConstantProvider'); +goog.require('Blockly.Theme'); goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.object'); /** * An object that handles creating and setting each of the SVG elements * used by the renderer. * @param {!SVGElement} root The root SVG element. + * @param {!Blockly.Theme.BlockStyle} style The style object to use for + * colouring. + * @param {!Blockly.geras.ConstantProvider} constants The renderer's constants. * @constructor - * @implements {Blockly.blockRendering.IPathObject} + * @extends {Blockly.blockRendering.PathObject} * @package */ -Blockly.geras.PathObject = function(root) { +Blockly.geras.PathObject = function(root, style, constants) { + /** + * The renderer's constant provider. + * @type {!Blockly.geras.ConstantProvider} + */ + this.constants_ = constants; + this.svgRoot = root; // The order of creation for these next three matters, because that @@ -66,23 +78,43 @@ Blockly.geras.PathObject = function(root) { */ this.svgPathLight = Blockly.utils.dom.createSvgElement('path', {'class': 'blocklyPathLight'}, this.svgRoot); + + /** + * The colour of the dark path on the block in '#RRGGBB' format. + * @type {string} + * @package + */ + this.colourDark = '#000000'; + + /** + * The style object to use when colouring block paths. + * @type {!Blockly.Theme.BlockStyle} + * @package + */ + this.style = style; +}; +Blockly.utils.object.inherits(Blockly.geras.PathObject, + Blockly.blockRendering.PathObject); + +/** + * @override + */ +Blockly.geras.PathObject.prototype.setPath = function(mainPath) { + this.svgPath.setAttribute('d', mainPath); + this.svgPathDark.setAttribute('d', mainPath); }; /** - * Set each of the paths generated by the renderer onto the respective SVG element. - * @param {string} mainPath The main path. + * Set the highlight path generated by the renderer onto the SVG element. * @param {string} highlightPath The highlight path. * @package */ -Blockly.geras.PathObject.prototype.setPaths = function(mainPath, highlightPath) { - this.svgPath.setAttribute('d', mainPath); - this.svgPathDark.setAttribute('d', mainPath); +Blockly.geras.PathObject.prototype.setHighlightPath = function(highlightPath) { this.svgPathLight.setAttribute('d', highlightPath); }; /** - * Flip the SVG paths in RTL. - * @package + * @override */ Blockly.geras.PathObject.prototype.flipRTL = function() { // Mirror the block's path. @@ -90,3 +122,63 @@ Blockly.geras.PathObject.prototype.flipRTL = function() { this.svgPathLight.setAttribute('transform', 'scale(-1 1)'); this.svgPathDark.setAttribute('transform', 'translate(1,1) scale(-1 1)'); }; + +/** + * @override + */ +Blockly.geras.PathObject.prototype.applyColour = function(block) { + this.svgPathLight.style.display = ''; + this.svgPathDark.style.display = ''; + this.svgPathLight.setAttribute('stroke', this.style.colourTertiary); + this.svgPathDark.setAttribute('fill', this.colourDark); + + Blockly.geras.PathObject.superClass_.applyColour.call(this, block); + + this.svgPath.setAttribute('stroke', 'none'); +}; + +/** + * @override + */ +Blockly.geras.PathObject.prototype.setStyle = function(blockStyle) { + this.style = blockStyle; + this.colourDark = + Blockly.utils.colour.blend('#000', this.style.colourPrimary, 0.2) || + this.colourDark; +}; + +/** + * @override + */ +Blockly.geras.PathObject.prototype.updateHighlighted = function(highlighted) { + if (highlighted) { + this.svgPath.setAttribute('filter', + 'url(#' + this.constants_.embossFilterId + ')'); + this.svgPathLight.style.display = 'none'; + } else { + this.svgPath.setAttribute('filter', 'none'); + this.svgPathLight.style.display = 'inline'; + } +}; + +/** + * @override + */ +Blockly.geras.PathObject.prototype.updateShadow_ = function(shadow) { + if (shadow) { + this.svgPathLight.style.display = 'none'; + this.svgPathDark.setAttribute('fill', this.style.colourSecondary); + this.svgPath.setAttribute('stroke', 'none'); + this.svgPath.setAttribute('fill', this.style.colourSecondary); + } +}; + +/** + * @override + */ +Blockly.geras.PathObject.prototype.updateDisabled_ = function(disabled) { + Blockly.geras.PathObject.superClass_.updateDisabled_.call(this, disabled); + if (disabled) { + this.svgPath.setAttribute('stroke', 'none'); + } +}; diff --git a/core/renderers/geras/renderer.js b/core/renderers/geras/renderer.js index fcd5e516d..32508972b 100644 --- a/core/renderers/geras/renderer.js +++ b/core/renderers/geras/renderer.js @@ -35,12 +35,13 @@ goog.require('Blockly.utils.object'); /** * The geras renderer. + * @param {string} name The renderer name. * @package * @constructor * @extends {Blockly.blockRendering.Renderer} */ -Blockly.geras.Renderer = function() { - Blockly.geras.Renderer.superClass_.constructor.call(this); +Blockly.geras.Renderer = function(name) { + Blockly.geras.Renderer.superClass_.constructor.call(this, name); /** * The renderer's highlight constant provider. @@ -98,12 +99,15 @@ Blockly.geras.Renderer.prototype.makeDrawer_ = function(block, info) { /** * Create a new instance of a renderer path object. * @param {!SVGElement} root The root SVG element. + * @param {!Blockly.Theme.BlockStyle} style The style object to use for + * colouring. * @return {!Blockly.geras.PathObject} The renderer path object. * @package * @override */ -Blockly.geras.Renderer.prototype.makePathObject = function(root) { - return new Blockly.geras.PathObject(root); +Blockly.geras.Renderer.prototype.makePathObject = function(root, style) { + return new Blockly.geras.PathObject(root, style, + /** @type {!Blockly.geras.ConstantProvider} */ (this.getConstants())); }; /** @@ -112,7 +116,7 @@ Blockly.geras.Renderer.prototype.makePathObject = function(root) { * provider. * @protected */ -Blockly.blockRendering.Renderer.prototype.makeHighlightConstants_ = function() { +Blockly.geras.Renderer.prototype.makeHighlightConstants_ = function() { return new Blockly.geras.HighlightConstantProvider( /** @type {!Blockly.blockRendering.ConstantProvider} */ (this.getConstants())); diff --git a/core/renderers/measurables/connections.js b/core/renderers/measurables/connections.js index d514a6439..5c095433d 100644 --- a/core/renderers/measurables/connections.js +++ b/core/renderers/measurables/connections.js @@ -36,7 +36,7 @@ goog.require('Blockly.utils.object'); * the block. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering * constants provider. - * @param {Blockly.RenderedConnection} connectionModel The connection object on + * @param {!Blockly.RenderedConnection} connectionModel The connection object on * the block that this represents. * @package * @constructor @@ -47,6 +47,7 @@ Blockly.blockRendering.Connection = function(constants, connectionModel) { constants); this.connectionModel = connectionModel; this.shape = this.constants_.shapeFor(connectionModel); + this.isDynamicShape = !!this.shape['isDynamic']; this.type |= Blockly.blockRendering.Types.CONNECTION; }; Blockly.utils.object.inherits(Blockly.blockRendering.Connection, @@ -67,22 +68,17 @@ Blockly.blockRendering.OutputConnection = function(constants, connectionModel) { Blockly.blockRendering.OutputConnection.superClass_.constructor.call(this, constants, connectionModel); this.type |= Blockly.blockRendering.Types.OUTPUT_CONNECTION; - this.height = this.shape.height; - this.width = this.shape.width; - this.connectionOffsetY = this.constants_.TAB_OFFSET_FROM_TOP; + + this.height = !this.isDynamicShape ? this.shape.height : 0; + this.width = !this.isDynamicShape ? this.shape.width : 0; this.startX = this.width; + + this.connectionOffsetY = this.constants_.TAB_OFFSET_FROM_TOP; + this.connectionOffsetX = 0; }; Blockly.utils.object.inherits(Blockly.blockRendering.OutputConnection, Blockly.blockRendering.Connection); -/** - * Whether or not the connection shape is dynamic. Dynamic shapes get their - * height from the block. - * @return {boolean} True if the connection shape is dynamic. - */ -Blockly.blockRendering.OutputConnection.prototype.isDynamic = function() { - return this.shape.isDynamic; -}; /** * An object containing information about the space a previous connection takes diff --git a/core/renderers/measurables/inputs.js b/core/renderers/measurables/inputs.js index 5ff02a77b..2272755d3 100644 --- a/core/renderers/measurables/inputs.js +++ b/core/renderers/measurables/inputs.js @@ -61,8 +61,6 @@ Blockly.blockRendering.InputConnection = function(constants, input) { this.connectedBlockHeight = 0; } - // TODO: change references to connectionModel, since that's on Connection. - this.connection = input.connection; this.connectionOffsetX = 0; this.connectionOffsetY = 0; }; @@ -87,8 +85,7 @@ Blockly.blockRendering.InlineInput = function(constants, input) { if (!this.connectedBlock) { this.height = this.constants_.EMPTY_INLINE_INPUT_HEIGHT; - this.width = this.shape.width + - this.constants_.EMPTY_INLINE_INPUT_PADDING; + this.width = this.constants_.EMPTY_INLINE_INPUT_PADDING; } else { // We allow the dark path to show on the parent block so that the child // block looks embossed. This takes up an extra pixel in both x and y. @@ -96,9 +93,18 @@ Blockly.blockRendering.InlineInput = function(constants, input) { this.height = this.connectedBlockHeight; } - this.connectionOffsetY = this.constants_.TAB_OFFSET_FROM_TOP; - this.connectionHeight = this.shape.height; - this.connectionWidth = this.shape.width; + this.connectionHeight = !this.isDynamicShape ? this.shape.height : + this.shape.height(this.height); + this.connectionWidth = !this.isDynamicShape ? this.shape.width : + this.shape.width(this.height); + if (!this.connectedBlock) { + this.width += this.connectionWidth * (this.isDynamicShape ? 2 : 1); + } + this.connectionOffsetY = this.isDynamicShape ? + this.shape.connectionOffsetY(this.connectionHeight) : + this.constants_.TAB_OFFSET_FROM_TOP; + this.connectionOffsetX = this.isDynamicShape ? + this.shape.connectionOffsetX(this.connectionWidth) : 0; }; Blockly.utils.object.inherits(Blockly.blockRendering.InlineInput, Blockly.blockRendering.InputConnection); @@ -127,8 +133,7 @@ Blockly.blockRendering.StatementInput = function(constants, input) { this.height = this.connectedBlockHeight + this.constants_.STATEMENT_BOTTOM_SPACER; } - this.width = this.constants_.NOTCH_OFFSET_LEFT + - this.shape.width; + this.width = this.constants_.STATEMENT_INPUT_NOTCH_OFFSET + this.shape.width; }; Blockly.utils.object.inherits(Blockly.blockRendering.StatementInput, Blockly.blockRendering.InputConnection); @@ -148,12 +153,12 @@ Blockly.blockRendering.ExternalValueInput = function(constants, input) { Blockly.blockRendering.ExternalValueInput.superClass_.constructor.call(this, constants, input); this.type |= Blockly.blockRendering.Types.EXTERNAL_VALUE_INPUT; - if (!this.connectedBlock) { this.height = this.shape.height; } else { this.height = - this.connectedBlockHeight - 2 * this.constants_.TAB_OFFSET_FROM_TOP; + this.connectedBlockHeight - this.constants_.TAB_OFFSET_FROM_TOP - + this.constants_.MEDIUM_PADDING; } this.width = this.shape.width + this.constants_.EXTERNAL_VALUE_INPUT_PADDING; diff --git a/core/renderers/measurables/row_elements.js b/core/renderers/measurables/row_elements.js index c445ce6b0..095449c05 100644 --- a/core/renderers/measurables/row_elements.js +++ b/core/renderers/measurables/row_elements.js @@ -91,7 +91,7 @@ Blockly.utils.object.inherits(Blockly.blockRendering.JaggedEdge, Blockly.blockRendering.Field = function(constants, field, parentInput) { Blockly.blockRendering.Field.superClass_.constructor.call(this, constants); this.field = field; - this.isEditable = field.isCurrentlyEditable(); + this.isEditable = field.EDITABLE; this.flipRtl = field.getFlipRtl(); this.type |= Blockly.blockRendering.Types.FIELD; diff --git a/core/renderers/measurables/rows.js b/core/renderers/measurables/rows.js index 2797cb7e8..6a86f3fa4 100644 --- a/core/renderers/measurables/rows.js +++ b/core/renderers/measurables/rows.js @@ -154,6 +154,13 @@ Blockly.blockRendering.Row = function(constants) { this.constants_ = constants; this.notchOffset = this.constants_.NOTCH_OFFSET_LEFT; + + /** + * Alignment of the row. + * @package + * @type {?number} + */ + this.align = null; }; /** @@ -280,17 +287,28 @@ Blockly.utils.object.inherits(Blockly.blockRendering.TopRow, /** * Returns whether or not the top row has a left square corner. * @param {!Blockly.BlockSvg} block The block whose top row this represents. - * @returns {boolean} Whether or not the top row has a left square corner. + * @return {boolean} Whether or not the top row has a left square corner. */ Blockly.blockRendering.TopRow.prototype.hasLeftSquareCorner = function(block) { - var hasHat = (block.hat ? block.hat === 'cap' : Blockly.BlockSvg.START_HAT) && - !block.outputConnection && !block.previousConnection; + var hasHat = (typeof block.hat !== 'undefined' ? + block.hat === 'cap' : this.constants_.ADD_START_HATS) && + !block.outputConnection && !block.previousConnection; var prevBlock = block.getPreviousBlock(); return !!block.outputConnection || hasHat || (prevBlock ? prevBlock.getNextBlock() == block : false); }; +/** + * Returns whether or not the top row has a right square corner. + * @param {!Blockly.BlockSvg} _block The block whose top row this represents. + * @return {boolean} Whether or not the top row has a right square corner. + */ +Blockly.blockRendering.TopRow.prototype.hasRightSquareCorner = function( + _block) { + return true; +}; + /** * @override */ @@ -322,6 +340,13 @@ Blockly.blockRendering.TopRow.prototype.startsWithElemSpacer = function() { return false; }; +/** + * @override + */ +Blockly.blockRendering.TopRow.prototype.endsWithElemSpacer = function() { + return false; +}; + /** * An object containing information about what elements are in the bottom row of * a block as well as spacing information for the top row. @@ -373,13 +398,23 @@ Blockly.utils.object.inherits(Blockly.blockRendering.BottomRow, /** * Returns whether or not the bottom row has a left square corner. * @param {!Blockly.BlockSvg} block The block whose bottom row this represents. - * @returns {boolean} Whether or not the bottom row has a left square corner. + * @return {boolean} Whether or not the bottom row has a left square corner. */ Blockly.blockRendering.BottomRow.prototype.hasLeftSquareCorner = function( block) { return !!block.outputConnection || !!block.getNextBlock(); }; +/** + * Returns whether or not the bottom row has a right square corner. + * @param {!Blockly.BlockSvg} _block The block whose bottom row this represents. + * @return {boolean} Whether or not the bottom row has a right square corner. + */ +Blockly.blockRendering.BottomRow.prototype.hasRightSquareCorner = function( + _block) { + return true; +}; + /** * @override */ @@ -412,6 +447,13 @@ Blockly.blockRendering.BottomRow.prototype.startsWithElemSpacer = function() { return false; }; +/** + * @override + */ +Blockly.blockRendering.BottomRow.prototype.endsWithElemSpacer = function() { + return false; +}; + /** * An object containing information about a spacer between two rows. * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering @@ -481,7 +523,8 @@ Blockly.blockRendering.InputRow.prototype.measure = function() { connectedBlockWidths += elem.connectedBlockWidth; } else if (Blockly.blockRendering.Types.isExternalInput(elem) && elem.connectedBlockWidth != 0) { - connectedBlockWidths += (elem.connectedBlockWidth - elem.connectionWidth); + connectedBlockWidths += (elem.connectedBlockWidth - + elem.connectionWidth); } } if (!(Blockly.blockRendering.Types.isSpacer(elem))) { diff --git a/core/renderers/measurables/types.js b/core/renderers/measurables/types.js index 02c3c48f6..5784e2d9e 100644 --- a/core/renderers/measurables/types.js +++ b/core/renderers/measurables/types.js @@ -28,34 +28,33 @@ goog.provide('Blockly.blockRendering.Types'); /** * Types of rendering elements. * @enum {number} - * @package */ Blockly.blockRendering.Types = { - NONE: 0, // None - FIELD: 1 << 0, // Field. - HAT: 1 << 1, // Hat. - ICON: 1 << 2, // Icon. - SPACER: 1 << 3, // Spacer. - BETWEEN_ROW_SPACER: 1 << 4, // Between Row Spacer. - IN_ROW_SPACER: 1 << 5, // In Row Spacer. + NONE: 0, // None + FIELD: 1 << 0, // Field. + HAT: 1 << 1, // Hat. + ICON: 1 << 2, // Icon. + SPACER: 1 << 3, // Spacer. + BETWEEN_ROW_SPACER: 1 << 4, // Between Row Spacer. + IN_ROW_SPACER: 1 << 5, // In Row Spacer. EXTERNAL_VALUE_INPUT: 1 << 6, // External Value Input. - INPUT: 1 << 7, // Input - INLINE_INPUT: 1 << 8, // Inline Input. - STATEMENT_INPUT: 1 << 9, // Statement Input. - CONNECTION: 1 << 10, // Connection. + INPUT: 1 << 7, // Input. + INLINE_INPUT: 1 << 8, // Inline Input. + STATEMENT_INPUT: 1 << 9, // Statement Input. + CONNECTION: 1 << 10, // Connection. PREVIOUS_CONNECTION: 1 << 11, // Previous Connection. - NEXT_CONNECTION: 1 << 12, // Next Connection. - OUTPUT_CONNECTION: 1 << 13, // Output Connection. - CORNER: 1 << 14, // Corner. - LEFT_SQUARE_CORNER: 1 << 15, // Square Corner. - LEFT_ROUND_CORNER: 1 << 16, // Round Corner. + NEXT_CONNECTION: 1 << 12, // Next Connection. + OUTPUT_CONNECTION: 1 << 13, // Output Connection. + CORNER: 1 << 14, // Corner. + LEFT_SQUARE_CORNER: 1 << 15, // Square Corner. + LEFT_ROUND_CORNER: 1 << 16, // Round Corner. RIGHT_SQUARE_CORNER: 1 << 17, // Right Square Corner. - RIGHT_ROUND_CORNER: 1 << 18, // Right Round Corner. - JAGGED_EDGE: 1 << 19, // Jagged Edge. - ROW: 1 << 20, // Row - TOP_ROW: 1 << 21, // Top Row. - BOTTOM_ROW: 1 << 22, // Bototm Row. - INPUT_ROW: 1 << 23, // Input Row. + RIGHT_ROUND_CORNER: 1 << 18, // Right Round Corner. + JAGGED_EDGE: 1 << 19, // Jagged Edge. + ROW: 1 << 20, // Row. + TOP_ROW: 1 << 21, // Top Row. + BOTTOM_ROW: 1 << 22, // Bottom Row. + INPUT_ROW: 1 << 23 // Input Row. }; /** @@ -135,7 +134,8 @@ Blockly.blockRendering.Types.isIcon = function(elem) { /** * Whether a measurable stores information about a spacer. - * @param {!Blockly.blockRendering.Measurable} elem The element to check. + * @param {!Blockly.blockRendering.Measurable|!Blockly.blockRendering.Row} elem + * The element to check. * @return {number} 1 if the object stores information about a spacer. * @package */ diff --git a/core/renderers/minimalist/renderer.js b/core/renderers/minimalist/renderer.js index 4eb3c3787..e475f6e1e 100644 --- a/core/renderers/minimalist/renderer.js +++ b/core/renderers/minimalist/renderer.js @@ -32,12 +32,13 @@ goog.require('Blockly.minimalist.RenderInfo'); /** * The minimalist renderer. + * @param {string} name The renderer name. * @package * @constructor * @extends {Blockly.blockRendering.Renderer} */ -Blockly.minimalist.Renderer = function() { - Blockly.minimalist.Renderer.superClass_.constructor.call(this); +Blockly.minimalist.Renderer = function(name) { + Blockly.minimalist.Renderer.superClass_.constructor.call(this, name); }; Blockly.utils.object.inherits(Blockly.minimalist.Renderer, Blockly.blockRendering.Renderer); diff --git a/core/renderers/thrasos/info.js b/core/renderers/thrasos/info.js index b8772cbb9..25014a805 100644 --- a/core/renderers/thrasos/info.js +++ b/core/renderers/thrasos/info.js @@ -114,7 +114,8 @@ Blockly.thrasos.RenderInfo.prototype.addElemSpacing_ = function() { Blockly.thrasos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { if (!prev) { // Between an editable field and the beginning of the row. - if (next && Blockly.blockRendering.Types.isField(next) && next.isEditable) { + if (next && Blockly.blockRendering.Types.isField(next) && + (/** @type Blockly.blockRendering.Field */ (next)).isEditable) { return this.constants_.MEDIUM_PADDING; } // Inline input at the beginning of the row. @@ -131,7 +132,8 @@ Blockly.thrasos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { // Spacing between a non-input and the end of the row. if (!Blockly.blockRendering.Types.isInput(prev) && !next) { // Between an editable field and the end of the row. - if (Blockly.blockRendering.Types.isField(prev) && prev.isEditable) { + if (Blockly.blockRendering.Types.isField(prev) && + (/** @type Blockly.blockRendering.Field */ (prev)).isEditable) { return this.constants_.MEDIUM_PADDING; } // Padding at the end of an icon-only row to make the block shape clearer. @@ -172,7 +174,8 @@ Blockly.thrasos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { if (!Blockly.blockRendering.Types.isInput(prev) && next && Blockly.blockRendering.Types.isInput(next)) { // Between an editable field and an input. - if (prev.isEditable) { + if (Blockly.blockRendering.Types.isField(prev) && + (/** @type Blockly.blockRendering.Field */ (prev)).isEditable) { if (Blockly.blockRendering.Types.isInlineInput(next)) { return this.constants_.SMALL_PADDING; } else if (Blockly.blockRendering.Types.isExternalInput(next)) { @@ -198,9 +201,9 @@ Blockly.thrasos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { // Spacing between an inline input and a field. if (Blockly.blockRendering.Types.isInlineInput(prev) && - next && !Blockly.blockRendering.Types.isInput(next)) { + next && Blockly.blockRendering.Types.isField(next)) { // Editable field after inline input. - if (next.isEditable) { + if ((/** @type Blockly.blockRendering.Field */ (next)).isEditable) { return this.constants_.MEDIUM_PADDING; } else { // Noneditable field after inline input. @@ -226,9 +229,10 @@ Blockly.thrasos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { } // Spacing between two fields of the same editability. - if (!Blockly.blockRendering.Types.isInput(prev) && - next && !Blockly.blockRendering.Types.isInput(next) && - (prev.isEditable == next.isEditable)) { + if (Blockly.blockRendering.Types.isField(prev) && + next && Blockly.blockRendering.Types.isField(next) && + ((/** @type Blockly.blockRendering.Field */ (prev)).isEditable == + (/** @type Blockly.blockRendering.Field */ (next)).isEditable)) { return this.constants_.LARGE_PADDING; } @@ -240,34 +244,6 @@ Blockly.thrasos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { return this.constants_.MEDIUM_PADDING; }; -/** - * @override - */ -Blockly.thrasos.RenderInfo.prototype.addAlignmentPadding_ = function(row, missingSpace) { - var firstSpacer = row.getFirstSpacer(); - var lastSpacer = row.getLastSpacer(); - 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. - lastSpacer.width += missingSpace; - } else if (row.align == Blockly.ALIGN_CENTRE) { - // Split the padding between the beginning and end of the row. - firstSpacer.width += missingSpace / 2; - lastSpacer.width += missingSpace / 2; - } else if (row.align == Blockly.ALIGN_RIGHT) { - // Add padding at the beginning of the row. - firstSpacer.width += missingSpace; - } else { - // Default to left-aligning. - lastSpacer.width += missingSpace; - } - row.width += missingSpace; -}; - /** * @override */ @@ -357,6 +333,13 @@ Blockly.thrasos.RenderInfo.prototype.finalize_ = function() { } this.recordElemPositions_(row); } + if (this.outputConnection && this.block_.nextConnection && + this.block_.nextConnection.isConnected()) { + // Include width of connected block in value to stack width measurement. + widestRowWithConnectedBlocks = + Math.max(widestRowWithConnectedBlocks, + this.block_.nextConnection.targetBlock().getHeightWidth().width); + } this.bottomRow.baseline = yCursor - this.bottomRow.descenderHeight; this.widthWithChildren = widestRowWithConnectedBlocks + this.startX; diff --git a/core/renderers/thrasos/renderer.js b/core/renderers/thrasos/renderer.js index 048cf2a5e..2e49886a3 100644 --- a/core/renderers/thrasos/renderer.js +++ b/core/renderers/thrasos/renderer.js @@ -30,12 +30,13 @@ goog.require('Blockly.utils.object'); /** * The thrasos renderer. + * @param {string} name The renderer name. * @package * @constructor * @extends {Blockly.blockRendering.Renderer} */ -Blockly.thrasos.Renderer = function() { - Blockly.thrasos.Renderer.superClass_.constructor.call(this); +Blockly.thrasos.Renderer = function(name) { + Blockly.thrasos.Renderer.superClass_.constructor.call(this, name); }; Blockly.utils.object.inherits(Blockly.thrasos.Renderer, Blockly.blockRendering.Renderer); diff --git a/core/renderers/zelos/constants.js b/core/renderers/zelos/constants.js index 66f06c47f..97ec307a8 100644 --- a/core/renderers/zelos/constants.js +++ b/core/renderers/zelos/constants.js @@ -25,6 +25,7 @@ goog.provide('Blockly.zelos.ConstantProvider'); goog.require('Blockly.blockRendering.ConstantProvider'); +goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.object'); goog.require('Blockly.utils.svgPaths'); @@ -40,6 +41,26 @@ Blockly.zelos.ConstantProvider = function() { this.GRID_UNIT = 4; + /** + * @override + */ + this.SMALL_PADDING = this.GRID_UNIT; + + /** + * @override + */ + this.MEDIUM_PADDING = 2 * this.GRID_UNIT; + + /** + * @override + */ + this.MEDIUM_LARGE_PADDING = 3 * this.GRID_UNIT; + + /** + * @override + */ + this.LARGE_PADDING = 4 * this.GRID_UNIT; + /** * @override */ @@ -59,17 +80,313 @@ Blockly.zelos.ConstantProvider = function() { * @override */ this.NOTCH_OFFSET_LEFT = 3 * this.GRID_UNIT; + + /** + * @override + */ + this.STATEMENT_INPUT_NOTCH_OFFSET = this.NOTCH_OFFSET_LEFT; + + /** + * @override + */ + this.MIN_BLOCK_WIDTH = 2 * this.GRID_UNIT; /** * @override */ this.MIN_BLOCK_HEIGHT = 12 * this.GRID_UNIT; + /** + * @override + */ + this.EMPTY_STATEMENT_INPUT_HEIGHT = 6 * this.GRID_UNIT; + /** * @override */ this.TAB_OFFSET_FROM_TOP = 0; + /** + * @override + */ + this.TOP_ROW_MIN_HEIGHT = this.GRID_UNIT; + + /** + * @override + */ + this.TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT = this.LARGE_PADDING; + + /** + * @override + */ + this.BOTTOM_ROW_MIN_HEIGHT = this.GRID_UNIT; + + /** + * @override + */ + this.BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT = 6 * this.GRID_UNIT; + + /** + * @override + */ + this.STATEMENT_BOTTOM_SPACER = -this.NOTCH_HEIGHT; + + /** + * Minimum statement input spacer width. + * @type {number} + */ + this.STATEMENT_INPUT_SPACER_MIN_WIDTH = 40 * this.GRID_UNIT; + + /** + * @override + */ + this.STATEMENT_INPUT_PADDING_LEFT = 4 * this.GRID_UNIT; + + /** + * @override + */ + this.EMPTY_INLINE_INPUT_PADDING = 4 * this.GRID_UNIT; + + /** + * @override + */ + this.EMPTY_INLINE_INPUT_HEIGHT = 8 * this.GRID_UNIT; + + /** + * @override + */ + this.DUMMY_INPUT_MIN_HEIGHT = 8 * this.GRID_UNIT; + + /** + * @override + */ + this.DUMMY_INPUT_SHADOW_MIN_HEIGHT = 6 * this.GRID_UNIT; + + /** + * @override + */ + this.CURSOR_WS_WIDTH = 20 * this.GRID_UNIT; + + /** + * @override + */ + this.CURSOR_COLOUR = '#ffa200'; + + /** + * Radius of the cursor for input and output connections. + * @type {number} + * @package + */ + this.CURSOR_RADIUS = 5; + + /** + * @override + */ + this.JAGGED_TEETH_HEIGHT = 0; + + /** + * @override + */ + this.JAGGED_TEETH_WIDTH = 0; + + /** + * @override + */ + this.START_HAT_HEIGHT = 22; + + /** + * @override + */ + this.START_HAT_WIDTH = 96; + + /** + * @enum {number} + * @override + */ + this.SHAPES = { + HEXAGONAL: 1, + ROUND: 2, + SQUARE: 3, + PUZZLE: 4, + NOTCH: 5 + }; + + /** + * Map of output/input shapes and the amount they should cause a block to be + * padded. Outer key is the outer shape, inner key is the inner shape. + * When a block with the outer shape contains an input block with the inner + * shape on its left or right edge, the block elements are aligned such that + * the padding specified is reached. + * @package + */ + this.SHAPE_IN_SHAPE_PADDING = { + 1: { // Outer shape: hexagon. + 0: 5 * this.GRID_UNIT, // Field in hexagon. + 1: 2 * this.GRID_UNIT, // Hexagon in hexagon. + 2: 5 * this.GRID_UNIT, // Round in hexagon. + 3: 5 * this.GRID_UNIT // Square in hexagon. + }, + 2: { // Outer shape: round. + 0: 3 * this.GRID_UNIT, // Field in round. + 1: 3 * this.GRID_UNIT, // Hexagon in round. + 2: 1 * this.GRID_UNIT, // Round in round. + 3: 2 * this.GRID_UNIT // Square in round. + }, + 3: { // Outer shape: square. + 0: 2 * this.GRID_UNIT, // Field in square. + 1: 2 * this.GRID_UNIT, // Hexagon in square. + 2: 2 * this.GRID_UNIT, // Round in square. + 3: 2 * this.GRID_UNIT // Square in square. + } + }; + + /** + * @override + */ + this.FULL_BLOCK_FIELDS = true; + + /** + * @override + */ + this.FIELD_TEXT_FONTSIZE = 3 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_TEXT_FONTWEIGHT = 'bold'; + + /** + * @override + */ + this.FIELD_TEXT_FONTFAMILY = + '"Helvetica Neue", "Segoe UI", Helvetica, sans-serif'; + + /** + * @override + */ + this.FIELD_TEXT_HEIGHT = 13.1; + + /** + * Used by positioning text on IE and Edge as they don't support + * dominant-baseline:center. + * @override + */ + this.FIELD_TEXT_BASELINE_Y = 13.1; + + /** + * @override + */ + this.FIELD_BORDER_RECT_RADIUS = this.CORNER_RADIUS; + + /** + * @override + */ + this.FIELD_BORDER_RECT_X_PADDING = 2 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_BORDER_RECT_Y_PADDING = 1 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_BORDER_RECT_HEIGHT = 8 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_DROPDOWN_BORDER_RECT_HEIGHT = 8 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW = true; + + /** + * @override + */ + this.FIELD_DROPDOWN_COLOURED_DIV = true; + + /** + * @override + */ + this.FIELD_DROPDOWN_SVG_ARROW = true; + + /** + * @override + */ + this.FIELD_DROPDOWN_SVG_ARROW_PADDING = this.FIELD_BORDER_RECT_X_PADDING; + + /** + * @override + */ + this.FIELD_TEXTINPUT_BOX_SHADOW = true; + + /** + * @override + */ + this.FIELD_TEXT_Y_OFFSET = Blockly.utils.userAgent.CHROME ? -.45 : 0; + + /** + * @override + */ + this.FIELD_COLOUR_FULL_BLOCK = true; + + /** + * @override + */ + this.FIELD_COLOUR_DEFAULT_WIDTH = 2 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_COLOUR_DEFAULT_HEIGHT = 4 * this.GRID_UNIT; + + /** + * @override + */ + this.FIELD_CHECKBOX_X_OFFSET = this.FIELD_BORDER_RECT_X_PADDING - 3; + + /** + * @override + */ + this.FIELD_CHECKBOX_Y_OFFSET = 22; + + /** + * @override + */ + this.FIELD_CHECKBOX_DEFAULT_WIDTH = 6 * this.GRID_UNIT; + + /** + * The ID of the highlight glow filter, or the empty string if no filter is + * set. + * @type {string} + * @package + */ + this.highlightGlowFilterId = ''; + + /** + * The element to use for a higlight glow, or null if not set. + * @type {SVGElement} + * @private + */ + this.highlightGlowFilter_ = null; + + /** + * The ID of the highlight glow filter, or the empty string if no filter is + * set. + * @type {string} + * @package + */ + this.replacementGlowFilterId = ''; + + /** + * The element to use for a higlight glow, or null if not set. + * @type {SVGElement} + * @private + */ + this.replacementGlowFilter_ = null; }; Blockly.utils.object.inherits(Blockly.zelos.ConstantProvider, Blockly.blockRendering.ConstantProvider); @@ -81,30 +398,78 @@ Blockly.zelos.ConstantProvider.prototype.init = function() { Blockly.zelos.ConstantProvider.superClass_.init.call(this); this.HEXAGONAL = this.makeHexagonal(); this.ROUNDED = this.makeRounded(); + this.SQUARED = this.makeSquared(); + + this.STATEMENT_INPUT_NOTCH_OFFSET += this.INSIDE_CORNERS.rightWidth; }; /** + * @override + */ +Blockly.zelos.ConstantProvider.prototype.dispose = function() { + Blockly.zelos.ConstantProvider.superClass_.dispose.call(this); + if (this.highlightGlowFilter_) { + Blockly.utils.dom.removeNode(this.highlightGlowFilter_); + } +}; + +/** + * @override + */ +Blockly.zelos.ConstantProvider.prototype.makeStartHat = function() { + var height = this.START_HAT_HEIGHT; + var width = this.START_HAT_WIDTH; + + var mainPath = + Blockly.utils.svgPaths.curve('c', + [ + Blockly.utils.svgPaths.point(25, -height), + Blockly.utils.svgPaths.point(71, -height), + Blockly.utils.svgPaths.point(width, 0) + ]); + return { + height: height, + width: width, + path: mainPath + }; +}; + +/** + * Create sizing and path information about a hexagonal shape. * @return {!Object} An object containing sizing and path information about * a hexagonal shape for connections. * @package */ Blockly.zelos.ConstantProvider.prototype.makeHexagonal = function() { + // The main path for the hexagonal connection shape is made out of two lines. + // The lines are defined with relative positons and require the block height. // The 'up' and 'down' versions of the paths are the same, but the Y sign - // flips. Forward and back are the signs to use to move the cursor in the - // direction that the path is being drawn. + // flips. The 'left' and 'right' versions of the path are also the same, but + // the X sign flips. function makeMainPath(height, up, right) { var width = height / 2; var forward = up ? -1 : 1; var direction = right ? -1 : 1; - - return Blockly.utils.svgPaths.lineTo(-1 * direction * width, forward * height / 2) + - Blockly.utils.svgPaths.lineTo(direction * width, forward * height / 2); + var dy = forward * height / 2; + return Blockly.utils.svgPaths.lineTo(-direction * width, dy) + + Blockly.utils.svgPaths.lineTo(direction * width, dy); } return { - width: 0, - height: 0, + type: this.SHAPES.HEXAGONAL, isDynamic: true, + width: function(height) { + return height / 2; + }, + height: function(height) { + return height; + }, + connectionOffsetY: function(connectionHeight) { + return connectionHeight / 2; + }, + connectionOffsetX: function(connectionWidth) { + return - connectionWidth; + }, pathDown: function(height) { return makeMainPath(height, false, false); }, @@ -121,14 +486,17 @@ Blockly.zelos.ConstantProvider.prototype.makeHexagonal = function() { }; /** + * Create sizing and path information about a rounded shape. * @return {!Object} An object containing sizing and path information about * a rounded shape for connections. * @package */ Blockly.zelos.ConstantProvider.prototype.makeRounded = function() { + // The main path for the rounded connection shape is made out of a single arc. + // The arc is defined with relative positions and requires the block height. // The 'up' and 'down' versions of the paths are the same, but the Y sign - // flips. Forward and back are the signs to use to move the cursor in the - // direction that the path is being drawn. + // flips. The 'up' and 'right' versions of the path flip the sweep-flag + // which moves the arc at negative angles. function makeMainPath(height, up, right) { var edgeWidth = height / 2; return Blockly.utils.svgPaths.arc('a', '0 0 ' + (up || right ? 1 : 0), edgeWidth, @@ -136,9 +504,74 @@ Blockly.zelos.ConstantProvider.prototype.makeRounded = function() { } return { - width: 0, - height: 0, + type: this.SHAPES.ROUND, isDynamic: true, + width: function(height) { + return height / 2; + }, + height: function(height) { + return height; + }, + connectionOffsetY: function(connectionHeight) { + return connectionHeight / 2; + }, + connectionOffsetX: function(connectionWidth) { + return - connectionWidth; + }, + pathDown: function(height) { + return makeMainPath(height, false, false); + }, + pathUp: function(height) { + return makeMainPath(height, true, false); + }, + pathRightDown: function(height) { + return makeMainPath(height, false, true); + }, + pathRightUp: function(height) { + return makeMainPath(height, false, true); + }, + }; +}; + +/** + * Create sizing and path information about a squared shape. + * @return {!Object} An object containing sizing and path information about + * a squared shape for connections. + * @package + */ +Blockly.zelos.ConstantProvider.prototype.makeSquared = function() { + var radius = this.CORNER_RADIUS; + + // The main path for the squared connection shape is made out of two corners + // and a single line in-between (a and v). These are defined in relative + // positions and require the height of the block. + // The 'left' and 'right' versions of the paths are the same, but the Y sign + // flips. The 'up' and 'down' versions of the path determine where the corner + // point is placed and in-turn the direction of the corners. + function makeMainPath(height, up, right) { + var innerHeight = height - radius * 2; + return Blockly.utils.svgPaths.arc('a', '0 0,1', radius, + Blockly.utils.svgPaths.point((up ? -1 : 1) * radius, (up ? -1 : 1) * radius)) + + Blockly.utils.svgPaths.lineOnAxis('v', (right ? 1 : -1) * innerHeight) + + Blockly.utils.svgPaths.arc('a', '0 0,1', radius, + Blockly.utils.svgPaths.point((up ? 1 : -1) * radius, (up ? -1 : 1) * radius)); + } + + return { + type: this.SHAPES.SQUARE, + isDynamic: true, + width: function(_height) { + return radius; + }, + height: function(height) { + return height; + }, + connectionOffsetY: function(connectionHeight) { + return connectionHeight / 2; + }, + connectionOffsetX: function(connectionWidth) { + return - connectionWidth; + }, pathDown: function(height) { return makeMainPath(height, false, false); }, @@ -160,9 +593,21 @@ Blockly.zelos.ConstantProvider.prototype.makeRounded = function() { Blockly.zelos.ConstantProvider.prototype.shapeFor = function( connection) { var checks = connection.getCheck(); + if (!checks && connection.targetConnection) { + checks = connection.targetConnection.getCheck(); + } switch (connection.type) { case Blockly.INPUT_VALUE: case Blockly.OUTPUT_VALUE: + var outputShape = connection.getSourceBlock().getOutputShape(); + // If the block has an ouput shape set, use that instead. + if (outputShape != null) { + switch (outputShape) { + case this.SHAPES.HEXAGONAL: return this.HEXAGONAL; + case this.SHAPES.ROUND: return this.ROUNDED; + case this.SHAPES.SQUARE: return this.SQUARED; + } + } // Includes doesn't work in IE. if (checks && checks.indexOf('Boolean') != -1) { return this.HEXAGONAL; @@ -173,7 +618,7 @@ Blockly.zelos.ConstantProvider.prototype.shapeFor = function( if (checks && checks.indexOf('String') != -1) { return this.ROUNDED; } - return this.PUZZLE_TAB; + return this.ROUNDED; case Blockly.PREVIOUS_STATEMENT: case Blockly.NEXT_STATEMENT: return this.NOTCH; @@ -241,11 +686,11 @@ Blockly.zelos.ConstantProvider.prototype.makeNotch = function() { ); } - // TODO: Find a relationship between width and path var pathLeft = makeMainPath(1); var pathRight = makeMainPath(-1); return { + type: this.SHAPES.NOTCH, width: width, height: height, pathLeft: pathLeft, @@ -253,52 +698,6 @@ Blockly.zelos.ConstantProvider.prototype.makeNotch = function() { }; }; -/** - * @return {!Object} An object containing sizing and path information about - * outside corners. - * @package - */ -Blockly.zelos.ConstantProvider.prototype.makeOutsideCorners = function() { - var radius = this.CORNER_RADIUS; - /** - * SVG path for drawing the rounded top-left corner. - * @const - */ - var topLeft = - Blockly.utils.svgPaths.moveBy(0, radius) + - Blockly.utils.svgPaths.arc('a', '0 0,1', radius, - Blockly.utils.svgPaths.point(radius, -radius)); - - /** - * SVG path for drawing the rounded top-right corner. - * @const - */ - var topRight = - Blockly.utils.svgPaths.arc('a', '0 0,1', radius, - Blockly.utils.svgPaths.point(radius, radius)); - - /** - * SVG path for drawing the rounded bottom-left corner. - * @const - */ - var bottomLeft = Blockly.utils.svgPaths.arc('a', '0 0,1', radius, - Blockly.utils.svgPaths.point(-radius, -radius)); - - /** - * SVG path for drawing the rounded bottom-right corner. - * @const - */ - var bottomRight = Blockly.utils.svgPaths.arc('a', '0 0,1', radius, - Blockly.utils.svgPaths.point(-radius, radius)); - - return { - topLeft: topLeft, - topRight: topRight, - bottomRight: bottomRight, - bottomLeft: bottomLeft - }; -}; - /** * @override */ @@ -328,3 +727,188 @@ Blockly.zelos.ConstantProvider.prototype.makeInsideCorners = function() { pathBottomRight: innerBottomRightCorner }; }; + +/** + * @override + */ +Blockly.zelos.ConstantProvider.prototype.generateSecondaryColour_ = function( + colour) { + return Blockly.utils.colour.blend('#000', colour, 0.15) || colour; +}; + +/** + * @override + */ +Blockly.zelos.ConstantProvider.prototype.generateTertiaryColour_ = function( + colour) { + return Blockly.utils.colour.blend('#000', colour, 0.25) || colour; +}; + +/** + * @override + */ +Blockly.zelos.ConstantProvider.prototype.createDom = function(svg) { + Blockly.zelos.ConstantProvider.superClass_.createDom.call(this, svg); + /* + + ... filters go here ... + + */ + var defs = Blockly.utils.dom.createSvgElement('defs', {}, svg); + // Using a dilate distorts the block shape. + // Instead use a gaussian blur, and then set all alpha to 1 with a transfer. + var highlightGlowFilter = Blockly.utils.dom.createSvgElement('filter', + { + 'id': 'blocklyHighlightGlowFilter' + this.randomIdentifier_, + 'height': '160%', + 'width': '180%', + y: '-30%', + x: '-40%' + }, + defs); + Blockly.utils.dom.createSvgElement('feGaussianBlur', + { + 'in': 'SourceGraphic', + 'stdDeviation': 0.5 // TODO: configure size in theme. + }, + highlightGlowFilter); + // Set all gaussian blur pixels to 1 opacity before applying flood + var highlightComponentTransfer = Blockly.utils.dom.createSvgElement( + 'feComponentTransfer', {'result': 'outBlur'}, highlightGlowFilter); + 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' + }, + highlightComponentTransfer); + // Color the highlight + Blockly.utils.dom.createSvgElement('feFlood', + { + 'flood-color': '#fff200', // TODO: configure colour in theme. + 'flood-opacity': 1, + 'result': 'outColor' + }, + highlightGlowFilter); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'outColor', 'in2': 'outBlur', + 'operator': 'in', 'result': 'outGlow' + }, + highlightGlowFilter); + this.highlightGlowFilterId = highlightGlowFilter.id; + this.highlightGlowFilter_ = highlightGlowFilter; + + // Using a dilate distorts the block shape. + // Instead use a gaussian blur, and then set all alpha to 1 with a transfer. + var replacementGlowFilter = Blockly.utils.dom.createSvgElement('filter', + { + 'id': 'blocklyReplacementGlowFilter' + this.randomIdentifier_, + 'height': '160%', + 'width': '180%', + y: '-30%', + x: '-40%' + }, + defs); + Blockly.utils.dom.createSvgElement('feGaussianBlur', + { + 'in': 'SourceGraphic', + 'stdDeviation': 2 // TODO: configure size in theme. + }, + replacementGlowFilter); + // Set all gaussian blur pixels to 1 opacity before applying flood + var replacementComponentTransfer = Blockly.utils.dom.createSvgElement( + 'feComponentTransfer', {'result': 'outBlur'}, replacementGlowFilter); + 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' + }, + replacementComponentTransfer); + // Color the highlight + Blockly.utils.dom.createSvgElement('feFlood', + { + 'flood-color': '#fff200', // TODO: configure colour in theme. + 'flood-opacity': 1, + 'result': 'outColor' + }, + replacementGlowFilter); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'outColor', 'in2': 'outBlur', + 'operator': 'in', 'result': 'outGlow' + }, + replacementGlowFilter); + Blockly.utils.dom.createSvgElement('feComposite', + { + 'in': 'SourceGraphic', 'in2': 'outGlow', + 'operator': 'over', + }, + replacementGlowFilter); + this.replacementGlowFilterId = replacementGlowFilter.id; + this.replacementGlowFilter_ = replacementGlowFilter; +}; + +/** + * @override + */ +Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(name) { + var selector = '.' + name + '-renderer'; + return [ + /* eslint-disable indent */ + // Fields. + selector + ' .blocklyText {', + 'fill: #fff;', + 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', + 'font-size: ' + this.FIELD_TEXT_FONTSIZE + 'pt;', + 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', + '}', + selector + ' .blocklyNonEditableText>rect:not(.blocklyDropdownRect),', + selector + ' .blocklyEditableText>rect:not(.blocklyDropdownRect) {', + 'fill: ' + this.FIELD_BORDER_RECT_COLOUR + ';', + '}', + selector + ' .blocklyNonEditableText>text,', + selector + ' .blocklyEditableText>text,', + selector + ' .blocklyNonEditableText>g>text,', + selector + ' .blocklyEditableText>g>text {', + 'fill: #575E75;', + '}', + + // Editable field hover. + selector + ' .blocklyDraggable:not(.blocklyDisabled)', + ' .blocklyEditableText:not(.editing):hover>rect ,', + selector + ' .blocklyDraggable:not(.blocklyDisabled)', + ' .blocklyEditableText:not(.editing):hover>.blocklyPath {', + 'stroke: #fff;', + 'stroke-width: 2;', + '}', + + // Text field input. + selector + ' .blocklyHtmlInput {', + 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', + 'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';', + 'color: #575E75;', + '}', + + // Dropdown field. + selector + ' .blocklyDropdownText {', + 'fill: #fff !important;', + '}', + // Widget and Dropdown Div + selector + '.blocklyWidgetDiv .goog-menuitem,', + selector + '.blocklyDropDownDiv .goog-menuitem {', + 'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';', + '}', + selector + '.blocklyDropDownDiv .goog-menuitem-content {', + 'color: #fff;', + '}', + + // Connection highlight. + selector + ' .blocklyHighlightedConnectionPath {', + 'stroke: #fff200;', + '}', + + // Disabled outline paths. + selector + ' .blocklyDisabled > .blocklyOutlinePath {', + 'fill: url(#blocklyDisabledPattern' + this.randomIdentifier_ + ')', + '}', + /* eslint-enable indent */ + ]; +}; diff --git a/core/renderers/zelos/drawer.js b/core/renderers/zelos/drawer.js index 584c3d286..22580d233 100644 --- a/core/renderers/zelos/drawer.js +++ b/core/renderers/zelos/drawer.js @@ -29,6 +29,8 @@ goog.require('Blockly.blockRendering.Types'); goog.require('Blockly.utils.object'); goog.require('Blockly.zelos.RenderInfo'); +goog.requireType('Blockly.zelos.PathObject'); + /** * An object that draws a block based on the given rendering information. @@ -46,12 +48,39 @@ Blockly.utils.object.inherits(Blockly.zelos.Drawer, Blockly.blockRendering.Drawer); +/** + * @override + */ +Blockly.zelos.Drawer.prototype.draw = function() { + var pathObject = + /** @type {!Blockly.zelos.PathObject} */ (this.block_.pathObject); + pathObject.beginDrawing(); + this.hideHiddenIcons_(); + this.drawOutline_(); + this.drawInternals_(); + + pathObject.setPath(this.outlinePath_ + '\n' + this.inlinePath_); + if (this.info_.RTL) { + pathObject.flipRTL(); + } + if (Blockly.blockRendering.useDebugger) { + this.block_.renderingDebugger.drawDebug(this.block_, this.info_); + } + this.recordSizeOnBlock_(); + if (this.info_.outputConnection) { + // Store the output connection shape type for parent blocks to use during + // rendering. + pathObject.outputShapeType = this.info_.outputConnection.shape.type; + } + pathObject.endDrawing(); +}; + /** * @override */ Blockly.zelos.Drawer.prototype.drawOutline_ = function() { if (this.info_.outputConnection && - this.info_.outputConnection.isDynamic()) { + this.info_.outputConnection.isDynamicShape) { this.drawFlatTop_(); this.drawRightDynamicConnection_(); this.drawFlatBottom_(); @@ -61,71 +90,6 @@ Blockly.zelos.Drawer.prototype.drawOutline_ = function() { } }; -/** - * Add steps for the top corner of the block, taking into account - * details such as hats and rounded corners. - * @protected - */ -Blockly.zelos.Drawer.prototype.drawTop_ = function() { - var topRow = this.info_.topRow; - var elements = topRow.elements; - - this.positionPreviousConnection_(); - this.outlinePath_ += - Blockly.utils.svgPaths.moveBy(topRow.xPos, this.info_.startY); - for (var i = 0, elem; (elem = elements[i]); i++) { - if (Blockly.blockRendering.Types.isLeftRoundedCorner(elem)) { - this.outlinePath_ += - this.constants_.OUTSIDE_CORNERS.topLeft; - } else if (Blockly.blockRendering.Types.isRightRoundedCorner(elem)) { - this.outlinePath_ += - this.constants_.OUTSIDE_CORNERS.topRight; - } else if (Blockly.blockRendering.Types.isPreviousConnection(elem)) { - this.outlinePath_ += elem.shape.pathLeft; - } else if (Blockly.blockRendering.Types.isHat(elem)) { - this.outlinePath_ += this.constants_.START_HAT.path; - } else if (Blockly.blockRendering.Types.isSpacer(elem)) { - this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('h', elem.width); - } - // No branch for a square corner because it's a no-op. - } - this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('v', topRow.height); -}; - - -/** - * Add steps for the bottom edge of a block, possibly including a notch - * for the next connection - * @protected - */ -Blockly.zelos.Drawer.prototype.drawBottom_ = function() { - var bottomRow = this.info_.bottomRow; - var elems = bottomRow.elements; - this.positionNextConnection_(); - - var rightCornerYOffset = 0; - var outlinePath = ''; - for (var i = elems.length - 1, elem; (elem = elems[i]); i--) { - if (Blockly.blockRendering.Types.isNextConnection(elem)) { - outlinePath += elem.shape.pathRight; - } else if (Blockly.blockRendering.Types.isLeftSquareCorner(elem)) { - outlinePath += Blockly.utils.svgPaths.lineOnAxis('H', bottomRow.xPos); - } else if (Blockly.blockRendering.Types.isLeftRoundedCorner(elem)) { - outlinePath += this.constants_.OUTSIDE_CORNERS.bottomLeft; - } else if (Blockly.blockRendering.Types.isRightRoundedCorner(elem)) { - outlinePath += this.constants_.OUTSIDE_CORNERS.bottomRight; - rightCornerYOffset = this.constants_.INSIDE_CORNERS.rightHeight; - } else if (Blockly.blockRendering.Types.isSpacer(elem)) { - outlinePath += Blockly.utils.svgPaths.lineOnAxis('h', elem.width * -1); - } - } - - this.outlinePath_ += - Blockly.utils.svgPaths.lineOnAxis('V', - bottomRow.baseline - rightCornerYOffset); - this.outlinePath_ += outlinePath; -}; - /** * Add steps for the right side of a row that does not have value or * statement input connections. @@ -134,18 +98,21 @@ Blockly.zelos.Drawer.prototype.drawBottom_ = function() { * @protected */ Blockly.zelos.Drawer.prototype.drawRightSideRow_ = function(row) { - if (row.type & Blockly.blockRendering.Types.getType('BEFORE_STATEMENT_SPACER_ROW')) { - var remainingHeight = row.height - this.constants_.INSIDE_CORNERS.rightWidth; + if (row.height <= 0) { + return; + } + if (row.precedesStatement || row.followsStatement) { + var cornerHeight = this.constants_.INSIDE_CORNERS.rightHeight; + var remainingHeight = row.height - + (row.precedesStatement ? cornerHeight : 0); this.outlinePath_ += + (row.followsStatement ? + this.constants_.INSIDE_CORNERS.pathBottomRight : '') + (remainingHeight > 0 ? - Blockly.utils.svgPaths.lineOnAxis('V', row.yPos + remainingHeight) : '') + - this.constants_.INSIDE_CORNERS.pathTopRight; - } else if (row.type & Blockly.blockRendering.Types.getType('AFTER_STATEMENT_SPACER_ROW')) { - var remainingHeight = row.height - this.constants_.INSIDE_CORNERS.rightWidth; - this.outlinePath_ += - this.constants_.INSIDE_CORNERS.pathBottomRight + - (remainingHeight > 0 ? - Blockly.utils.svgPaths.lineOnAxis('V', row.yPos + row.height) : ''); + Blockly.utils.svgPaths + .lineOnAxis('V', row.yPos + remainingHeight) : '') + + (row.precedesStatement ? + this.constants_.INSIDE_CORNERS.pathTopRight : ''); } else { this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('V', row.yPos + row.height); @@ -208,6 +175,56 @@ Blockly.zelos.Drawer.prototype.drawFlatBottom_ = function() { * @override */ Blockly.zelos.Drawer.prototype.drawInlineInput_ = function(input) { - // Don't draw an inline input. this.positionInlineInputConnection_(input); + + var inputName = input.input.name; + if (input.connectedBlock || this.info_.isInsertionMarker) { + return; + } + + var width = input.width - (input.connectionWidth * 2); + var height = input.height; + var yPos = input.centerline - height / 2; + + var connectionRight = input.xPos + input.connectionWidth; + + var outlinePath = Blockly.utils.svgPaths.moveTo(connectionRight, yPos) + + Blockly.utils.svgPaths.lineOnAxis('h', width) + + input.shape.pathRightDown(input.height) + + Blockly.utils.svgPaths.lineOnAxis('h', -width) + + input.shape.pathUp(input.height) + + 'z'; + this.block_.pathObject.setOutlinePath(inputName, outlinePath); +}; + +/** + * @override + */ +Blockly.zelos.Drawer.prototype.drawStatementInput_ = function(row) { + var input = row.getLastInput(); + // Where to start drawing the notch, which is on the right side in LTR. + var x = input.xPos + input.notchOffset + input.shape.width; + + var innerTopLeftCorner = + input.shape.pathRight + + Blockly.utils.svgPaths.lineOnAxis('h', + -(input.notchOffset - this.constants_.INSIDE_CORNERS.width)) + + this.constants_.INSIDE_CORNERS.pathTop; + + var innerHeight = + row.height - (2 * this.constants_.INSIDE_CORNERS.height); + + var innerBottomLeftCorner = + this.constants_.INSIDE_CORNERS.pathBottom + + Blockly.utils.svgPaths.lineOnAxis('h', + (input.notchOffset - this.constants_.INSIDE_CORNERS.width)) + + (input.connectedBottomNextConnection ? '' : input.shape.pathLeft); + + this.outlinePath_ += Blockly.utils.svgPaths.lineOnAxis('H', x) + + innerTopLeftCorner + + Blockly.utils.svgPaths.lineOnAxis('v', innerHeight) + + innerBottomLeftCorner + + Blockly.utils.svgPaths.lineOnAxis('H', row.xPos + row.width); + + this.positionStatementInputConnection_(row); }; diff --git a/core/renderers/zelos/info.js b/core/renderers/zelos/info.js index c852afb70..c80c6b5f3 100644 --- a/core/renderers/zelos/info.js +++ b/core/renderers/zelos/info.js @@ -38,13 +38,12 @@ goog.require('Blockly.blockRendering.RoundCorner'); goog.require('Blockly.blockRendering.Row'); goog.require('Blockly.blockRendering.SquareCorner'); goog.require('Blockly.blockRendering.SpacerRow'); -goog.require('Blockly.blockRendering.StatementInput'); goog.require('Blockly.blockRendering.TopRow'); goog.require('Blockly.blockRendering.Types'); goog.require('Blockly.utils.object'); -goog.require('Blockly.zelos.AfterStatementSpacerRow'); -goog.require('Blockly.zelos.BeforeStatementSpacerRow'); goog.require('Blockly.zelos.BottomRow'); +goog.require('Blockly.zelos.RightConnectionShape'); +goog.require('Blockly.zelos.StatementInput'); goog.require('Blockly.zelos.TopRow'); @@ -77,6 +76,25 @@ Blockly.zelos.RenderInfo = function(renderer, block) { * @override */ this.bottomRow = new Blockly.zelos.BottomRow(this.constants_); + + /** + * @override + */ + this.isInline = true; + + /** + * Whether the block should be rendered as a multi-line block, either because + * it's not inline or because it has been collapsed. + * @type {boolean} + */ + this.isMultiRow = !block.getInputsInline() || block.isCollapsed(); + + /** + * An object with rendering information about the right connection shape. + * @type {Blockly.zelos.RightConnectionShape} + */ + this.rightSide = this.outputConnection ? + new Blockly.zelos.RightConnectionShape(this.constants_) : null; }; Blockly.utils.object.inherits(Blockly.zelos.RenderInfo, Blockly.blockRendering.RenderInfo); @@ -91,41 +109,52 @@ Blockly.zelos.RenderInfo.prototype.getRenderer = function() { }; /** - * Create all non-spacer elements that belong on the top row. - * @package * @override */ -Blockly.zelos.RenderInfo.prototype.populateTopRow_ = function() { - Blockly.zelos.RenderInfo.superClass_.populateTopRow_.call(this); - - var rightSquareCorner = this.topRow.hasRightSquareCorner(this.block_); - - if (rightSquareCorner) { - this.topRow.elements.push( - new Blockly.blockRendering.SquareCorner(this.constants_, 'right')); - } else { - this.topRow.elements.push( - new Blockly.blockRendering.RoundCorner(this.constants_, 'right')); - } +Blockly.zelos.RenderInfo.prototype.measure = function() { + // Modifing parent measure method to add `adjustXPosition_`. + this.createRows_(); + this.addElemSpacing_(); + this.addRowSpacing_(); + this.adjustXPosition_(); + this.computeBounds_(); + this.alignRowElements_(); + this.finalize_(); }; /** - * Create all non-spacer elements that belong on the bottom row. - * @package * @override */ -Blockly.zelos.RenderInfo.prototype.populateBottomRow_ = function() { - Blockly.zelos.RenderInfo.superClass_.populateBottomRow_.call(this); - - var rightSquareCorner = this.bottomRow.hasRightSquareCorner(this.block_); - - if (rightSquareCorner) { - this.bottomRow.elements.push( - new Blockly.blockRendering.SquareCorner(this.constants_, 'right')); - } else { - this.bottomRow.elements.push( - new Blockly.blockRendering.RoundCorner(this.constants_, 'right')); +Blockly.zelos.RenderInfo.prototype.shouldStartNewRow_ = function(input, + lastInput) { + // If this is the first input, just add to the existing row. + // That row is either empty or has some icons in it. + if (!lastInput) { + return false; } + // A statement input or an input following one always gets a new row. + if (input.type == Blockly.NEXT_STATEMENT || + lastInput.type == Blockly.NEXT_STATEMENT) { + return true; + } + // Value and dummy inputs get new row if inputs are not inlined. + if (input.type == Blockly.INPUT_VALUE || input.type == Blockly.DUMMY_INPUT) { + return !this.isInline || this.isMultiRow; + } + return false; +}; + + +/** + * @override + */ +Blockly.zelos.RenderInfo.prototype.getDesiredRowWidth_ = function(row) { + if (row.hasStatement) { + var rightCornerWidth = this.constants_.INSIDE_CORNERS.rightWidth || 0; + return this.width - this.startX - rightCornerWidth; + } + return Blockly.zelos.RenderInfo.superClass_.getDesiredRowWidth_.call(this, + row); }; /** @@ -135,178 +164,31 @@ Blockly.zelos.RenderInfo.prototype.getInRowSpacing_ = function(prev, next) { if (!prev || !next) { // No need for padding at the beginning or end of the row if the // output shape is dynamic. - if (this.outputConnection && this.outputConnection.isDynamic()) { + if (this.outputConnection && this.outputConnection.isDynamicShape) { return this.constants_.NO_PADDING; } } if (!prev) { - // Between an editable field and the beginning of the row. - if (next && Blockly.blockRendering.Types.isField(next) && next.isEditable) { - return this.constants_.MEDIUM_PADDING; - } - // Inline input at the beginning of the row. - if (next && Blockly.blockRendering.Types.isInlineInput(next)) { - return this.constants_.MEDIUM_LARGE_PADDING; - } + // Statement input padding. if (next && Blockly.blockRendering.Types.isStatementInput(next)) { return this.constants_.STATEMENT_INPUT_PADDING_LEFT; } - // Anything else at the beginning of the row. - return this.constants_.LARGE_PADDING; } - - // Spacing between a non-input and the end of the row. - if (!Blockly.blockRendering.Types.isInput(prev) && !next) { - // Between an editable field and the end of the row. - if (Blockly.blockRendering.Types.isField(prev) && prev.isEditable) { - return this.constants_.MEDIUM_PADDING; - } - // Padding at the end of an icon-only row to make the block shape clearer. - if (Blockly.blockRendering.Types.isIcon(prev)) { - return (this.constants_.LARGE_PADDING * 2) + 1; - } - if (Blockly.blockRendering.Types.isHat(prev)) { - return this.constants_.NO_PADDING; - } - // Establish a minimum width for a block with a previous or next connection. - if (Blockly.blockRendering.Types.isPreviousOrNextConnection(prev)) { - return this.constants_.LARGE_PADDING; - } - // Between rounded corner and the end of the row. - if (Blockly.blockRendering.Types.isLeftRoundedCorner(prev)) { - return this.constants_.MIN_BLOCK_WIDTH; - } - // Between a right rounded corner and the end of the row. - if (Blockly.blockRendering.Types.isRightRoundedCorner(prev)) { - return this.constants_.NO_PADDING; - } - // Between a jagged edge and the end of the row. - if (Blockly.blockRendering.Types.isJaggedEdge(prev)) { - return this.constants_.NO_PADDING; - } - // Between noneditable fields and icons and the end of the row. - return this.constants_.LARGE_PADDING; - } - - // Between inputs and the end of the row. - if (Blockly.blockRendering.Types.isInput(prev) && !next) { - if (Blockly.blockRendering.Types.isExternalInput(prev)) { - return this.constants_.NO_PADDING; - } else if (Blockly.blockRendering.Types.isInlineInput(prev)) { - return this.constants_.LARGE_PADDING; - } else if (Blockly.blockRendering.Types.isStatementInput(prev)) { - return this.constants_.NO_PADDING; - } - } - - // Spacing between a non-input and an input. - if (!Blockly.blockRendering.Types.isInput(prev) && - next && Blockly.blockRendering.Types.isInput(next)) { - // Between an editable field and an input. - if (prev.isEditable) { - if (Blockly.blockRendering.Types.isInlineInput(next)) { - return this.constants_.SMALL_PADDING; - } else if (Blockly.blockRendering.Types.isExternalInput(next)) { - return this.constants_.SMALL_PADDING; - } - } else { - if (Blockly.blockRendering.Types.isInlineInput(next)) { - return this.constants_.MEDIUM_LARGE_PADDING; - } else if (Blockly.blockRendering.Types.isExternalInput(next)) { - return this.constants_.MEDIUM_LARGE_PADDING; - } else if (Blockly.blockRendering.Types.isStatementInput(next)) { - return this.constants_.LARGE_PADDING; - } - } - return this.constants_.LARGE_PADDING - 1; - } - - // Spacing between an icon and an icon or field. - if (Blockly.blockRendering.Types.isIcon(prev) && - next && !Blockly.blockRendering.Types.isInput(next)) { - return this.constants_.LARGE_PADDING; - } - - // Spacing between an inline input and a field. - if (Blockly.blockRendering.Types.isInlineInput(prev) && - next && !Blockly.blockRendering.Types.isInput(next)) { - // Editable field after inline input. - if (next.isEditable) { - return this.constants_.MEDIUM_PADDING; - } else { - // Noneditable field after inline input. - return this.constants_.LARGE_PADDING; - } - } - - if (Blockly.blockRendering.Types.isLeftSquareCorner(prev) && next) { - // Spacing between a hat and a corner - if (Blockly.blockRendering.Types.isHat(next)) { - return this.constants_.NO_PADDING; - } - // Spacing between a square corner and a previous or next connection - if (Blockly.blockRendering.Types.isPreviousConnection(next) || - Blockly.blockRendering.Types.isNextConnection(next)) { - return next.notchOffset; - } - } - // Spacing between a rounded corner and a previous or next connection. - if (Blockly.blockRendering.Types.isLeftRoundedCorner(prev) && next) { + if (prev && Blockly.blockRendering.Types.isLeftRoundedCorner(prev) && next) { if (Blockly.blockRendering.Types.isPreviousConnection(next) || - Blockly.blockRendering.Types.isNextConnection(next)) { + Blockly.blockRendering.Types.isNextConnection(next)) { return next.notchOffset - this.constants_.CORNER_RADIUS; } } - - // Spacing between two fields of the same editability. - if (!Blockly.blockRendering.Types.isInput(prev) && - next && !Blockly.blockRendering.Types.isInput(next) && - (prev.isEditable == next.isEditable)) { - return this.constants_.LARGE_PADDING; + // Spacing between a square corner and a hat. + if (prev && Blockly.blockRendering.Types.isLeftSquareCorner(prev) && next && + Blockly.blockRendering.Types.isHat(next)) { + return this.constants_.NO_PADDING; } - - // Spacing between anything and a jagged edge. - if (next && Blockly.blockRendering.Types.isJaggedEdge(next)) { - return this.constants_.LARGE_PADDING; - } - return this.constants_.MEDIUM_PADDING; }; -/** - * Create a spacer row to go between prev and next, and set its size. - * @param {?Blockly.blockRendering.Row} prev The previous row, or null. - * @param {?Blockly.blockRendering.Row} next The next row, or null. - * @return {!Blockly.blockRendering.SpacerRow} The newly created spacer row. - * @protected - */ -Blockly.zelos.RenderInfo.prototype.makeSpacerRow_ = function(prev, next) { - var height = this.getSpacerRowHeight_(prev, next); - var width = this.getSpacerRowWidth_(prev, next); - if (Blockly.blockRendering.Types.isInputRow(next) && next.hasStatement) { - var spacer = - new Blockly.zelos.BeforeStatementSpacerRow( - this.constants_, - Math.max(height, this.constants_.INSIDE_CORNERS.rightHeight || 0), - width); - } else if (Blockly.blockRendering.Types.isInputRow(prev) && prev.hasStatement) { - var spacer = - new Blockly.zelos.AfterStatementSpacerRow( - this.constants_, - Math.max(height, this.constants_.INSIDE_CORNERS.rightHeight || 0), - width); - } else { - var spacer = new Blockly.blockRendering.SpacerRow( - this.constants_, height, width); - } - if (prev.hasStatement) { - spacer.followsStatement = true; - } - return spacer; -}; - - /** * @override */ @@ -317,30 +199,375 @@ Blockly.zelos.RenderInfo.prototype.getSpacerRowHeight_ = function( Blockly.blockRendering.Types.isBottomRow(next)) { return this.constants_.EMPTY_BLOCK_SPACER_HEIGHT; } + var followsStatement = + Blockly.blockRendering.Types.isInputRow(prev) && prev.hasStatement; + var precedesStatement = + Blockly.blockRendering.Types.isInputRow(next) && next.hasStatement; + if (precedesStatement || followsStatement) { + var cornerHeight = this.constants_.INSIDE_CORNERS.rightHeight || 0; + var height = Math.max(this.constants_.MEDIUM_PADDING, + Math.max(this.constants_.NOTCH_HEIGHT, cornerHeight)); + return precedesStatement && followsStatement ? + Math.max(height, this.constants_.DUMMY_INPUT_MIN_HEIGHT) : height; + } // Top and bottom rows act as a spacer so we don't need any extra padding. - if ((Blockly.blockRendering.Types.isTopRow(prev) && !prev.hasPreviousConnection)) { + if ((Blockly.blockRendering.Types.isTopRow(prev))) { + if (!prev.hasPreviousConnection && !this.outputConnection) { + return this.constants_.SMALL_PADDING; + } return this.constants_.NO_PADDING; } - if ((Blockly.blockRendering.Types.isBottomRow(next) && !next.hasNextConnection)) { + if ((Blockly.blockRendering.Types.isBottomRow(next))) { + if (!this.outputConnection) { + return this.constants_.SMALL_PADDING; + } return this.constants_.NO_PADDING; } return this.constants_.MEDIUM_PADDING; }; /** - * Modify the given row to add the given amount of padding around its fields. - * The exact location of the padding is based on the alignment property of the - * last input in the field. - * @param {Blockly.blockRendering.Row} row The row to add padding to. - * @param {number} missingSpace How much padding to add. - * @protected + * @override + */ +Blockly.zelos.RenderInfo.prototype.getSpacerRowWidth_ = function(prev, next) { + var width = this.width - this.startX; + if ((Blockly.blockRendering.Types.isInputRow(prev) && prev.hasStatement) || + (Blockly.blockRendering.Types.isInputRow(next) && next.hasStatement)) { + return Math.max(width, this.constants_.STATEMENT_INPUT_SPACER_MIN_WIDTH); + } + return width; +}; + +/** + * @override + */ +Blockly.zelos.RenderInfo.prototype.getElemCenterline_ = function(row, elem) { + if (row.hasStatement && !Blockly.blockRendering.Types.isSpacer(elem)) { + return row.yPos + this.constants_.EMPTY_STATEMENT_INPUT_HEIGHT / 2; + } + return Blockly.zelos.RenderInfo.superClass_.getElemCenterline_.call(this, + row, elem); +}; + +/** + * @override + */ +Blockly.zelos.RenderInfo.prototype.addInput_ = function(input, activeRow) { + // If we have two dummy inputs on the same row, one aligned left and the other + // right, keep track of the right aligned dummy input so we can add padding + // later. + if (input.type == Blockly.DUMMY_INPUT && activeRow.hasDummyInput && + activeRow.align == Blockly.ALIGN_LEFT && + input.align == Blockly.ALIGN_RIGHT) { + activeRow.rightAlignedDummyInput = input; + } + // Non-dummy inputs have visual representations onscreen. + if (this.isInline && input.type == Blockly.INPUT_VALUE) { + activeRow.elements.push( + new Blockly.blockRendering.InlineInput(this.constants_, input)); + activeRow.hasInlineInput = true; + } else if (input.type == Blockly.NEXT_STATEMENT) { + activeRow.elements.push( + new Blockly.zelos.StatementInput(this.constants_, input)); + activeRow.hasStatement = true; + } else if (input.type == Blockly.INPUT_VALUE) { + activeRow.elements.push( + new Blockly.blockRendering.ExternalValueInput(this.constants_, input)); + activeRow.hasExternalInput = true; + } else if (input.type == Blockly.DUMMY_INPUT) { + // Dummy inputs have no visual representation, but the information is still + // important. + activeRow.minHeight = Math.max(activeRow.minHeight, + input.getSourceBlock() && input.getSourceBlock().isShadow() ? + this.constants_.DUMMY_INPUT_SHADOW_MIN_HEIGHT : + this.constants_.DUMMY_INPUT_MIN_HEIGHT); + activeRow.hasDummyInput = true; + } + if (activeRow.align == null) { + activeRow.align = input.align; + } +}; + +/** + * @override */ Blockly.zelos.RenderInfo.prototype.addAlignmentPadding_ = function(row, missingSpace) { - var lastSpacer = row.getLastSpacer(); - if (lastSpacer) { - lastSpacer.width += missingSpace; - row.width += missingSpace; + if (row.rightAlignedDummyInput) { + var alignmentDivider; + for (var i = 0, elem; (elem = row.elements[i]); i++) { + if (Blockly.blockRendering.Types.isSpacer(elem)) { + alignmentDivider = elem; + } + if (Blockly.blockRendering.Types.isField(elem) && + elem.parentInput == row.rightAlignedDummyInput) { + break; + } + } + if (alignmentDivider) { + alignmentDivider.width += missingSpace; + row.width += missingSpace; + return; + } + } + Blockly.zelos.RenderInfo.superClass_.addAlignmentPadding_.call(this, row, + missingSpace); +}; + +/** + * Adjust the x position of fields to bump all non-label fields in the first row + * past the notch position. This must be called before ``computeBounds`` is + * called. + * @protected + */ +Blockly.zelos.RenderInfo.prototype.adjustXPosition_ = function() { + var notchTotalWidth = this.constants_.NOTCH_OFFSET_LEFT + + this.constants_.NOTCH_WIDTH; + var minXPos = notchTotalWidth; + // Run through every input row on the block and only apply bump logic to the + // first input row (if the block has prev connection) and every input row that + // has a prev and next notch. + for (var i = 2; i < this.rows.length - 1; i += 2) { + var prevSpacer = this.rows[i - 1]; + var row = this.rows[i]; + var nextSpacer = this.rows[i + 1]; + + var hasPrevNotch = i == 2 ? + !!this.topRow.hasPreviousConnection : !!prevSpacer.followsStatement; + var hasNextNotch = i + 2 >= this.rows.length - 1 ? + !!this.bottomRow.hasNextConnection : !!nextSpacer.precedesStatement; + + if (Blockly.blockRendering.Types.isInputRow(row) && row.hasStatement) { + row.measure(); + minXPos = row.width - row.getLastInput().width + notchTotalWidth; + } else if (hasPrevNotch && (i == 2 || hasNextNotch) && + Blockly.blockRendering.Types.isInputRow(row) && !row.hasStatement) { + var xCursor = row.xPos; + var prevInRowSpacer = null; + for (var j = 0, elem; (elem = row.elements[j]); j++) { + if (Blockly.blockRendering.Types.isSpacer(elem)) { + prevInRowSpacer = elem; + } + if (prevInRowSpacer && (Blockly.blockRendering.Types.isField(elem) || + Blockly.blockRendering.Types.isInput(elem))) { + if (xCursor < minXPos && + !(Blockly.blockRendering.Types.isField(elem) && + (elem.field instanceof Blockly.FieldLabel || + elem.field instanceof Blockly.FieldImage))) { + var difference = minXPos - xCursor; + prevInRowSpacer.width += difference; + } + } + xCursor += elem.width; + } + } + } +}; + +/** + * Finalize the output connection info. In particular, set the height of the + * output connection to match that of the block. For the right side, add a + * right connection shape element and have it match the dimensions of the + * output connection. + * @protected + */ +Blockly.zelos.RenderInfo.prototype.finalizeOutputConnection_ = function() { + // Dynamic output connections depend on the height of the block. + if (!this.outputConnection || !this.outputConnection.isDynamicShape) { + return; + } + var yCursor = 0; + // Determine the block height. + for (var i = 0, row; (row = this.rows[i]); i++) { + row.yPos = yCursor; + yCursor += row.height; + } + this.height = yCursor; + + // Adjust the height of the output connection. + var connectionHeight = this.outputConnection.shape.height(yCursor); + var connectionWidth = this.outputConnection.shape.width(yCursor); + + this.outputConnection.height = connectionHeight; + this.outputConnection.width = connectionWidth; + this.outputConnection.startX = connectionWidth; + this.outputConnection.connectionOffsetY = + this.outputConnection.shape.connectionOffsetY(connectionHeight); + this.outputConnection.connectionOffsetX = + this.outputConnection.shape.connectionOffsetX(connectionWidth); + + // Adjust right side measurable. + this.rightSide.height = connectionHeight; + this.rightSide.width = connectionWidth; + this.rightSide.centerline = connectionHeight / 2; + this.rightSide.xPos = this.width + connectionWidth; + + this.startX = connectionWidth; + this.width += connectionWidth * 2; + this.widthWithChildren += connectionWidth * 2; +}; + +/** + * Finalize horizontal alignment of elements on the block. In particular, + * reduce the implicit spacing created by the left and right output connection + * shapes by adding setting negative spacing onto the leftmost and rightmost + * spacers. + * @protected + */ +Blockly.zelos.RenderInfo.prototype.finalizeHorizontalAlignment_ = function() { + if (!this.outputConnection) { + return; + } + var totalNegativeSpacing = 0; + for (var i = 0, row; (row = this.rows[i]); i++) { + if (!Blockly.blockRendering.Types.isInputRow(row)) { + continue; + } + var firstElem = row.elements[1]; + var lastElem = row.elements[row.elements.length - 2]; + var leftNegPadding = this.getNegativeSpacing_(firstElem); + var rightNegPadding = this.getNegativeSpacing_(lastElem); + totalNegativeSpacing = leftNegPadding + rightNegPadding; + var minBlockWidth = this.constants_.MIN_BLOCK_WIDTH + + this.outputConnection.width * 2; + if (this.width - totalNegativeSpacing < minBlockWidth) { + // Maintain a minimum block width, split negative spacing between left + // and right edge. + totalNegativeSpacing = this.width - minBlockWidth; + leftNegPadding = totalNegativeSpacing / 2; + rightNegPadding = totalNegativeSpacing / 2; + } + // Add a negative spacer on the start and end of the block. + row.elements.unshift(new Blockly.blockRendering.InRowSpacer(this.constants_, + -leftNegPadding)); + row.elements.push(new Blockly.blockRendering.InRowSpacer(this.constants_, + -rightNegPadding)); + } + if (totalNegativeSpacing) { + this.width -= totalNegativeSpacing; + this.widthWithChildren -= totalNegativeSpacing; + this.rightSide.xPos -= totalNegativeSpacing; + for (var i = 0, row; (row = this.rows[i]); i++) { + if (Blockly.blockRendering.Types.isTopRow(row) || + Blockly.blockRendering.Types.isBottomRow(row)) { + row.elements[1].width -= totalNegativeSpacing; + } + row.width -= totalNegativeSpacing; + row.widthWithConnectedBlocks -= totalNegativeSpacing; + } + } +}; + +/** + * Calculate the spacing to reduce the left and right edges by based on the + * outer and inner connection shape. + * @param {Blockly.blockRendering.Measurable} elem The first or last element on + * a block. + * @return {number} The amount of spacing to reduce the first or last spacer. + * @protected + */ +Blockly.zelos.RenderInfo.prototype.getNegativeSpacing_ = function(elem) { + if (!elem) { + return 0; + } + var connectionWidth = this.outputConnection.width; + var outerShape = this.outputConnection.shape.type; + var constants = + /** @type {!Blockly.zelos.ConstantProvider} */ (this.constants_); + if (this.isMultiRow && this.inputRowNum_ > 1) { + switch (outerShape) { + case constants.SHAPES.ROUND: + // Special case for multi-row round reporter blocks. + var radius = this.height / 2; + var topPadding = this.constants_.SMALL_PADDING; + var roundPadding = radius * + (1 - Math.sin(Math.acos((radius - topPadding) / radius))); + return connectionWidth - roundPadding; + default: + return 0; + } + } + if (Blockly.blockRendering.Types.isInlineInput(elem)) { + var innerShape = elem.connectedBlock ? + elem.connectedBlock.pathObject.outputShapeType : + elem.shape.type; + // Special case for hexagonal output. + if (outerShape == constants.SHAPES.HEXAGONAL && + outerShape != innerShape) { + return 0; + } + return connectionWidth - + this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][innerShape]; + } else if (Blockly.blockRendering.Types.isField(elem)) { + // Special case for text inputs. + if (outerShape == constants.SHAPES.ROUND && + elem.field instanceof Blockly.FieldTextInput) { + return connectionWidth - (2.75 * constants.GRID_UNIT); + } + return connectionWidth - + this.constants_.SHAPE_IN_SHAPE_PADDING[outerShape][0]; + } else if (Blockly.blockRendering.Types.isIcon(elem)) { + return this.constants_.SMALL_PADDING; + } + return 0; +}; + +/** + * Finalize vertical alignment of rows on a block. In particular, reduce the + * implicit spacing when a non-shadow block is connected to any of an input + * row's inline inputs. + * @protected + */ +Blockly.zelos.RenderInfo.prototype.finalizeVerticalAlignment_ = function() { + if (this.outputConnection) { + return; + } + // Run through every input row on the block and only apply tight nesting logic + // to input rows that have a prev and next notch. + for (var i = 2; i < this.rows.length - 1; i += 2) { + var prevSpacer = this.rows[i - 1]; + var row = this.rows[i]; + var nextSpacer = this.rows[i + 1]; + + var firstRow = i == 2; + var hasPrevNotch = firstRow ? + !!this.topRow.hasPreviousConnection : !!prevSpacer.followsStatement; + var hasNextNotch = i + 2 >= this.rows.length - 1 ? + !!this.bottomRow.hasNextConnection : !!nextSpacer.precedesStatement; + + if (hasPrevNotch) { + var hasSingleTextOrImageField = row.elements.length == 3 && + (row.elements[1].field instanceof Blockly.FieldLabel || + row.elements[1].field instanceof Blockly.FieldImage); + if (!firstRow && hasSingleTextOrImageField) { + // Remove some padding if we have a single image or text field. + prevSpacer.height -= this.constants_.SMALL_PADDING; + nextSpacer.height -= this.constants_.SMALL_PADDING; + row.height -= this.constants_.MEDIUM_PADDING; + } else if (!firstRow && !hasNextNotch) { + // Add a small padding so the notch doesn't clash with inputs/fields. + prevSpacer.height += this.constants_.SMALL_PADDING; + } else if (hasNextNotch) { + // Determine if the input row has non-shadow connected blocks. + var hasNonShadowConnectedBlocks = false; + var MIN_VERTICAL_TIGHTNESTING_HEIGHT = 40; + for (var j = 0, elem; (elem = row.elements[j]); j++) { + if (Blockly.blockRendering.Types.isInlineInput(elem) && + elem.connectedBlock && !elem.connectedBlock.isShadow() && + elem.connectedBlock.getHeightWidth().height >= + MIN_VERTICAL_TIGHTNESTING_HEIGHT) { + hasNonShadowConnectedBlocks = true; + break; + } + } + // Apply tight-nesting if we have both a prev and next notch and the + // block has non-shadow connected blocks. + if (hasNonShadowConnectedBlocks) { + prevSpacer.height -= this.constants_.SMALL_PADDING; + nextSpacer.height -= this.constants_.SMALL_PADDING; + } + } + } } }; @@ -348,38 +575,8 @@ Blockly.zelos.RenderInfo.prototype.addAlignmentPadding_ = function(row, * @override */ Blockly.zelos.RenderInfo.prototype.finalize_ = function() { - // Performance note: this could be combined with the draw pass, if the time - // that this takes is excessive. But it shouldn't be, because it only - // accesses and sets properties that already exist on the objects. - var yCursor = 0; - for (var i = 0, row; (row = this.rows[i]); i++) { - row.yPos = yCursor; - yCursor += row.height; - } - // Dynamic output connections depend on the height of the block. Adjust the - // height and width of the connection, and then adjust the startX and width of the - // block accordingly. - var outputConnectionWidth = 0; - if (this.outputConnection && !this.outputConnection.height) { - this.outputConnection.height = yCursor; - outputConnectionWidth = yCursor; // Twice the width to account for the right side. - this.outputConnection.width = outputConnectionWidth / 2; - } - this.startX += outputConnectionWidth / 2; - this.width += outputConnectionWidth; - - var widestRowWithConnectedBlocks = 0; - for (var i = 0, row; (row = this.rows[i]); i++) { - row.xPos = this.startX; - - widestRowWithConnectedBlocks = - Math.max(widestRowWithConnectedBlocks, row.widthWithConnectedBlocks); - this.recordElemPositions_(row); - } - - this.widthWithChildren = widestRowWithConnectedBlocks + this.startX; - - this.height = yCursor; - this.startY = this.topRow.capline; - this.bottomRow.baseline = yCursor - this.bottomRow.descenderHeight; + this.finalizeOutputConnection_(); + this.finalizeHorizontalAlignment_(); + this.finalizeVerticalAlignment_(); + Blockly.zelos.RenderInfo.superClass_.finalize_.call(this); }; diff --git a/core/renderers/zelos/marker_svg.js b/core/renderers/zelos/marker_svg.js new file mode 100644 index 000000000..7d262043f --- /dev/null +++ b/core/renderers/zelos/marker_svg.js @@ -0,0 +1,148 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Methods for graphically rendering a marker as SVG. + * @author samelh@microsoft.com (Sam El-Husseini) + */ +'use strict'; + +goog.provide('Blockly.zelos.MarkerSvg'); + +goog.require('Blockly.blockRendering.MarkerSvg'); + + +/** + * Class to draw a marker. + * @param {!Blockly.WorkspaceSvg} workspace The workspace the marker belongs to. + * @param {!Blockly.blockRendering.ConstantProvider} constants The constants for + * the renderer. + * @param {!Blockly.Marker} marker The marker to draw. + * @constructor + * @extends {Blockly.blockRendering.MarkerSvg} + */ +Blockly.zelos.MarkerSvg = function(workspace, constants, marker) { + Blockly.zelos.MarkerSvg.superClass_.constructor.call( + this, workspace, constants, marker); +}; +Blockly.utils.object.inherits(Blockly.zelos.MarkerSvg, + Blockly.blockRendering.MarkerSvg); + +/** + * @override + */ +Blockly.zelos.MarkerSvg.prototype.showWithInput_ = function(curNode) { + var block = /** @type {!Blockly.BlockSvg} */ (curNode.getSourceBlock()); + var connection = curNode.getLocation(); + var offsetInBlock = connection.getOffsetInBlock(); + + var y = offsetInBlock.y + this.constants_.CURSOR_RADIUS; + + this.positionCircle_(offsetInBlock.x, y); + this.setParent_(block); + this.showCurrent_(); +}; + +/** + * Draw a rectangle around the block. + * @param {!Blockly.ASTNode} curNode The current node of the marker. + */ +Blockly.zelos.MarkerSvg.prototype.showWithBlock_ = function(curNode) { + var block = /** @type {!Blockly.BlockSvg} */ (curNode.getLocation()); + + // Gets the height and width of entire stack. + var heightWidth = block.getHeightWidth(); + + // Add padding so that being on a stack looks different than being on a block. + this.positionRect_(0, 0, heightWidth.width, heightWidth.height); + this.setParent_(block); + this.showCurrent_(); +}; + +/** + * Position the circle we use for input and output connections. + * @param {number} x The x position of the circle. + * @param {number} y The y position of the circle. + * @private + */ +Blockly.zelos.MarkerSvg.prototype.positionCircle_ = function(x, y) { + this.markerCircle_.setAttribute('cx', x); + this.markerCircle_.setAttribute('cy', 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 + */ +Blockly.zelos.MarkerSvg.prototype.hide = function() { + Blockly.zelos.MarkerSvg.superClass_.hide.call(this); + this.markerCircle_.style.display = 'none'; +}; + +/** + * @override + */ +Blockly.zelos.MarkerSvg.prototype.createDomInternal_ = function() { + /* This markup will be generated and added to the .svgGroup_: + + + + + + */ + + Blockly.zelos.MarkerSvg.superClass_.createDomInternal_.call(this); + + this.markerCircle_ = Blockly.utils.dom.createSvgElement('circle', { + 'r': this.constants_.CURSOR_RADIUS, + 'style': 'display: none', + 'fill': this.colour_, + 'stroke': this.colour_, + 'stroke-width': this.constants_.CURSOR_STROKE_WIDTH + }, + this.markerSvg_); + + // Markers and stack cursors don't blink. + if (this.isCursor()) { + var blinkProperties = this.getBlinkProperties_(); + Blockly.utils.dom.createSvgElement('animate', blinkProperties, + this.markerCircle_); + } + + return this.markerSvg_; +}; + diff --git a/core/renderers/zelos/measurables/inputs.js b/core/renderers/zelos/measurables/inputs.js new file mode 100644 index 000000000..b7ecab505 --- /dev/null +++ b/core/renderers/zelos/measurables/inputs.js @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Zelos specific objects representing inputs with connections on + * a rendered block. + * @author samelh@google.com (Sam El-Husseini) + */ + +goog.provide('Blockly.zelos.StatementInput'); + +goog.require('Blockly.blockRendering.StatementInput'); +goog.require('Blockly.utils.object'); + + +/** + * An object containing information about the space a statement input takes up + * during rendering + * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering + * constants provider. + * @param {!Blockly.Input} input The statement input to measure and store + * information for. + * @package + * @constructor + * @extends {Blockly.blockRendering.StatementInput} + */ +Blockly.zelos.StatementInput = function(constants, input) { + Blockly.zelos.StatementInput.superClass_.constructor.call(this, + constants, input); + + if (this.connectedBlock) { + // Find the bottom-most connected block in the stack. + var block = this.connectedBlock; + while (block.getNextBlock()) { + block = block.getNextBlock(); + } + if (!block.nextConnection) { + this.height = this.connectedBlockHeight; + this.connectedBottomNextConnection = true; + } + } +}; +Blockly.utils.object.inherits(Blockly.zelos.StatementInput, + Blockly.blockRendering.StatementInput); diff --git a/core/renderers/zelos/measurables/row_elements.js b/core/renderers/zelos/measurables/row_elements.js new file mode 100644 index 000000000..ba6bb2de0 --- /dev/null +++ b/core/renderers/zelos/measurables/row_elements.js @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Zelos specific objects representing elements in a row of a + * rendered block. + * @author samelh@google.com (Sam El-Husseini) + */ + +goog.provide('Blockly.zelos.RightConnectionShape'); + +goog.require('Blockly.blockRendering.Measurable'); +goog.require('Blockly.blockRendering.Types'); +goog.require('Blockly.utils.object'); + + +/** + * An object containing information about the space a right connection shape + * takes up during rendering. + * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering + * constants provider. + * @package + * @constructor + * @extends {Blockly.blockRendering.Measurable} + */ +Blockly.zelos.RightConnectionShape = function(constants) { + Blockly.zelos.RightConnectionShape.superClass_.constructor.call(this, constants); + this.type |= Blockly.blockRendering.Types.getType('RIGHT_CONNECTION'); + // Size is dynamic + this.height = 0; + this.width = 0; +}; +Blockly.utils.object.inherits(Blockly.zelos.RightConnectionShape, + Blockly.blockRendering.Measurable); diff --git a/core/renderers/zelos/measurables/rows.js b/core/renderers/zelos/measurables/rows.js index 376fa90e3..64c5836b8 100644 --- a/core/renderers/zelos/measurables/rows.js +++ b/core/renderers/zelos/measurables/rows.js @@ -24,8 +24,6 @@ goog.provide('Blockly.zelos.BottomRow'); goog.provide('Blockly.zelos.TopRow'); -goog.provide('Blockly.zelos.AfterStatementSpacerRow'); -goog.provide('Blockly.zelos.BeforeStatementSpacerRow'); goog.require('Blockly.blockRendering.BottomRow'); goog.require('Blockly.blockRendering.TopRow'); @@ -64,16 +62,17 @@ Blockly.zelos.TopRow.prototype.endsWithElemSpacer = function() { * @override */ Blockly.zelos.TopRow.prototype.hasLeftSquareCorner = function(block) { - return !!block.outputConnection; + var hasHat = (typeof block.hat !== 'undefined' ? + block.hat === 'cap' : this.constants_.ADD_START_HATS) && + !block.outputConnection && !block.previousConnection; + return !!block.outputConnection || hasHat; }; /** - * Returns whether or not the top row has a right square corner. - * @param {!Blockly.BlockSvg} block The block whose top row this represents. - * @returns {boolean} Whether or not the top row has a left square corner. + * Render a round corner unless the block has an output connection. + * @override */ Blockly.zelos.TopRow.prototype.hasRightSquareCorner = function(block) { - // Render a round corner unless the block has an output connection. return !!block.outputConnection; }; @@ -110,51 +109,9 @@ Blockly.zelos.BottomRow.prototype.hasLeftSquareCorner = function(block) { }; /** - * Returns whether or not the bottom row has a right square corner. - * @param {!Blockly.BlockSvg} block The block whose bottom row this represents. - * @returns {boolean} Whether or not the bottom row has a left square corner. + * Render a round corner unless the block has an output connection. + * @override */ Blockly.zelos.BottomRow.prototype.hasRightSquareCorner = function(block) { - // Render a round corner unless the block has an output connection. return !!block.outputConnection; }; - -/** - * An object containing information about a row spacer that comes right - * before a statement input. - * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering - * constants provider. - * @param {number} height The height of the spacer. - * @param {number} width The width of the spacer. - * @package - * @constructor - * @extends {Blockly.blockRendering.SpacerRow} - */ -Blockly.zelos.BeforeStatementSpacerRow = function(constants, height, width) { - Blockly.zelos.BeforeStatementSpacerRow.superClass_.constructor.call( - this, constants, height, width); - this.type |= - Blockly.blockRendering.Types.getType('BEFORE_STATEMENT_SPACER_ROW'); -}; -Blockly.utils.object.inherits(Blockly.zelos.BeforeStatementSpacerRow, - Blockly.blockRendering.SpacerRow); - -/** - * An object containing information about a row spacer that comes right - * after a statement input. - * @param {!Blockly.blockRendering.ConstantProvider} constants The rendering - * constants provider. - * @param {number} height The height of the spacer. - * @param {number} width The width of the spacer. - * @package - * @constructor - * @extends {Blockly.blockRendering.SpacerRow} - */ -Blockly.zelos.AfterStatementSpacerRow = function(constants, height, width) { - Blockly.zelos.AfterStatementSpacerRow.superClass_.constructor.call( - this, constants, height, width); - this.type |= - Blockly.blockRendering.Types.getType('AFTER_STATEMENT_SPACER_ROW'); -}; -Blockly.utils.object.inherits(Blockly.zelos.AfterStatementSpacerRow, - Blockly.blockRendering.SpacerRow); diff --git a/core/renderers/zelos/path_object.js b/core/renderers/zelos/path_object.js new file mode 100644 index 000000000..70ced53b9 --- /dev/null +++ b/core/renderers/zelos/path_object.js @@ -0,0 +1,253 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview An object that owns a block's rendering SVG elements. + * @author samelh@google.com (Sam El-Husseini) + */ + +'use strict'; + +goog.provide('Blockly.zelos.PathObject'); + +goog.require('Blockly.blockRendering.PathObject'); +goog.require('Blockly.zelos.ConstantProvider'); +goog.require('Blockly.utils.dom'); +goog.require('Blockly.utils.object'); + + +/** + * An object that handles creating and setting each of the SVG elements + * used by the renderer. + * @param {!SVGElement} root The root SVG element. + * @param {!Blockly.Theme.BlockStyle} style The style object to use for + * colouring. + * @param {!Blockly.zelos.ConstantProvider} constants The renderer's constants. + * @constructor + * @extends {Blockly.blockRendering.PathObject} + * @package + */ +Blockly.zelos.PathObject = function(root, style, constants) { + Blockly.zelos.PathObject.superClass_.constructor.call(this, root, style, + constants); + + /** + * The renderer's constant provider. + * @type {!Blockly.zelos.ConstantProvider} + */ + this.constants_ = constants; + + /** + * The selected path of the block. + * @type {SVGElement} + * @private + */ + this.svgPathSelected_ = null; + + /** + * The outline paths on the block. + * @type {!Object.} + * @private + */ + this.outlines_ = {}; + + /** + * A set used to determine which outlines were used during a draw pass. The + * set is initialized with a reference to all the outlines in + * `this.outlines_`. Everytime we use an outline during the draw pass, the + * reference is removed from this set. + * @type {Object.} + * @private + */ + this.remainingOutlines_ = null; + + /** + * The type of block's output connection shape. This is set when a block with + * an output connection is drawn. + * @package + */ + this.outputShapeType = null; +}; +Blockly.utils.object.inherits(Blockly.zelos.PathObject, + Blockly.blockRendering.PathObject); + +/** + * @override + */ +Blockly.zelos.PathObject.prototype.setPath = function(pathString) { + Blockly.zelos.PathObject.superClass_.setPath.call(this, pathString); + if (this.svgPathSelected_) { + this.svgPathSelected_.setAttribute('d', pathString); + } +}; + +/** + * @override + */ +Blockly.zelos.PathObject.prototype.applyColour = function(block) { + Blockly.zelos.PathObject.superClass_.applyColour.call(this, block); + // Set shadow stroke colour. + if (block.isShadow() && block.getParent()) { + this.svgPath.setAttribute('stroke', block.getParent().style.colourTertiary); + } + + // Apply colour to outlines. + for (var i = 0, keys = Object.keys(this.outlines_), + key; (key = keys[i]); i++) { + this.outlines_[key].setAttribute('fill', this.style.colourTertiary); + } +}; + +/** + * @override + */ +Blockly.zelos.PathObject.prototype.flipRTL = function() { + Blockly.zelos.PathObject.superClass_.flipRTL.call(this); + // Mirror each input outline path. + for (var i = 0, keys = Object.keys(this.outlines_), + key; (key = keys[i]); i++) { + this.outlines_[key].setAttribute('transform', 'scale(-1 1)'); + } +}; + +/** + * @override + */ +Blockly.zelos.PathObject.prototype.updateSelected = function(enable) { + this.setClass_('blocklySelected', enable); + if (enable) { + if (!this.svgPathSelected_) { + this.svgPathSelected_ = + /** @type {!SVGElement} */ (this.svgPath.cloneNode(true)); + this.svgPathSelected_.setAttribute('fill', 'none'); + this.svgPathSelected_.setAttribute('filter', + 'url(#' + this.constants_.highlightGlowFilterId + ')'); + this.svgRoot.appendChild(this.svgPathSelected_); + } + } else { + if (this.svgPathSelected_) { + this.svgRoot.removeChild(this.svgPathSelected_); + this.svgPathSelected_ = null; + } + } +}; + +/** + * @override + */ +Blockly.zelos.PathObject.prototype.updateReplacementHighlight = function( + enable) { + this.setClass_('blocklyReplaceable', enable); + if (enable) { + this.svgPath.setAttribute('filter', + 'url(#' + this.constants_.replacementGlowFilterId + ')'); + } else { + this.svgPath.removeAttribute('filter'); + } +}; + +/** + * @override + */ +Blockly.zelos.PathObject.prototype.updateShapeForInputHighlight = function( + conn, enable) { + var name = conn.getParentInput().name; + var outlinePath = this.getOutlinePath_(name); + if (!outlinePath) { + return; + } + if (enable) { + outlinePath.setAttribute('filter', + 'url(#' + this.constants_.replacementGlowFilterId + ')'); + } else { + outlinePath.removeAttribute('filter'); + } +}; + +/** + * Method that's called when the drawer is about to draw the block. + * @package + */ +Blockly.zelos.PathObject.prototype.beginDrawing = function() { + this.remainingOutlines_ = {}; + for (var i = 0, keys = Object.keys(this.outlines_), + key; (key = keys[i]); i++) { + // The value set here isn't used anywhere, we are just using the + // object as a Set data structure. + this.remainingOutlines_[key] = 1; + } +}; + +/** + * Method that's called when the drawer is done drawing. + * @package + */ +Blockly.zelos.PathObject.prototype.endDrawing = function() { + // Go through all remaining outlines that were not used this draw pass, and + // remove them. + if (this.remainingOutlines_) { + for (var i = 0, keys = Object.keys(this.remainingOutlines_), + key; (key = keys[i]); i++) { + this.removeOutlinePath_(key); + } + } + this.remainingOutlines_ = null; +}; + +/** + * Set the path generated by the renderer for an outline path on the respective + * outline path SVG element. + * @param {string} name The input name. + * @param {string} pathString The path. + * @package + */ +Blockly.zelos.PathObject.prototype.setOutlinePath = function(name, pathString) { + var outline = this.getOutlinePath_(name); + outline.setAttribute('d', pathString); + outline.setAttribute('fill', this.style.colourTertiary); +}; + +/** + * Create's an outline path for the specified input. + * @param {string} name The input name. + * @return {!SVGElement} The SVG outline path. + * @private + */ +Blockly.zelos.PathObject.prototype.getOutlinePath_ = function(name) { + if (!this.outlines_[name]) { + this.outlines_[name] = Blockly.utils.dom.createSvgElement('path', { + 'class': 'blocklyOutlinePath', + // IE doesn't like paths without the data definition, set empty default + 'd': '' + }, + this.svgRoot); + } + if (this.remainingOutlines_) { + delete this.remainingOutlines_[name]; + } + return this.outlines_[name]; +}; + +/** + * Remove an outline path that is associated with the specified input. + * @param {string} name The input name. + * @private + */ +Blockly.zelos.PathObject.prototype.removeOutlinePath_ = function(name) { + this.outlines_[name].parentNode.removeChild(this.outlines_[name]); + delete this.outlines_[name]; +}; diff --git a/core/renderers/zelos/renderer.js b/core/renderers/zelos/renderer.js index 576697030..5f805baf8 100644 --- a/core/renderers/zelos/renderer.js +++ b/core/renderers/zelos/renderer.js @@ -28,17 +28,20 @@ goog.require('Blockly.blockRendering.Renderer'); goog.require('Blockly.utils.object'); goog.require('Blockly.zelos.ConstantProvider'); goog.require('Blockly.zelos.Drawer'); +goog.require('Blockly.zelos.PathObject'); goog.require('Blockly.zelos.RenderInfo'); +goog.require('Blockly.zelos.MarkerSvg'); /** * The zelos renderer. + * @param {string} name The renderer name. * @package * @constructor * @extends {Blockly.blockRendering.Renderer} */ -Blockly.zelos.Renderer = function() { - Blockly.zelos.Renderer.superClass_.constructor.call(this); +Blockly.zelos.Renderer = function(name) { + Blockly.zelos.Renderer.superClass_.constructor.call(this, name); }; Blockly.utils.object.inherits(Blockly.zelos.Renderer, Blockly.blockRendering.Renderer); @@ -78,4 +81,47 @@ Blockly.zelos.Renderer.prototype.makeDrawer_ = function(block, info) { /** @type {!Blockly.zelos.RenderInfo} */ (info)); }; +/** + * Create a new instance of the renderer's cursor drawer. + * @param {!Blockly.WorkspaceSvg} workspace The workspace the cursor belongs to. + * @param {!Blockly.Marker} marker The marker. + * @return {!Blockly.blockRendering.MarkerSvg} The object in charge of drawing + * the marker. + * @package + * @override + */ +Blockly.zelos.Renderer.prototype.makeMarkerDrawer = function( + workspace, marker) { + return new Blockly.zelos.MarkerSvg(workspace, this.getConstants(), marker); +}; + +/** + * Create a new instance of a renderer path object. + * @param {!SVGElement} root The root SVG element. + * @param {!Blockly.Theme.BlockStyle} style The style object to use for + * colouring. + * @return {!Blockly.zelos.PathObject} The renderer path object. + * @package + * @override + */ +Blockly.zelos.Renderer.prototype.makePathObject = function(root, style) { + return new Blockly.zelos.PathObject(root, style, + /** @type {!Blockly.zelos.ConstantProvider} */ (this.getConstants())); +}; + +/** + * @override + */ +Blockly.zelos.Renderer.prototype.shouldHighlightConnection = function(conn) { + return conn.type != Blockly.INPUT_VALUE && conn.type !== Blockly.OUTPUT_VALUE; +}; + +/** + * @override + */ +Blockly.zelos.Renderer.prototype.shouldInsertDraggedBlock = function(_block, + _conn) { + return false; +}; + Blockly.blockRendering.register('zelos', Blockly.zelos.Renderer); diff --git a/core/requires.js b/core/requires.js index 46a61706a..223bc7d6d 100644 --- a/core/requires.js +++ b/core/requires.js @@ -62,6 +62,7 @@ goog.require('Blockly.ZoomControls'); // None of these should be required when using advanced compilation since // individual block files should include the requirements they depend on. goog.require('Blockly.Mutator'); +goog.require('Blockly.Warning'); goog.require('Blockly.FieldAngle'); goog.require('Blockly.FieldCheckbox'); goog.require('Blockly.FieldColour'); @@ -73,13 +74,20 @@ 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. // Others may be chosen using Blockly.inject's "renderer" configuration. goog.require('Blockly.geras.Renderer'); -// goog.require('Blockly.thrasos.Renderer'); -// goog.require('Blockly.zelos.Renderer'); +goog.require('Blockly.thrasos.Renderer'); +goog.require('Blockly.zelos.Renderer'); // The debug renderer, which shows simplified versions of the blocks for // developer use. // goog.require('Blockly.blockRendering.Debug'); @@ -88,5 +96,7 @@ goog.require('Blockly.geras.Renderer'); // Classic is the default theme. goog.require('Blockly.Themes.Classic'); goog.require('Blockly.Themes.Dark'); +goog.require('Blockly.Themes.Deuteranopia'); goog.require('Blockly.Themes.HighContrast'); -goog.require('Blockly.Themes.Modern'); +goog.require('Blockly.Themes.Tritanopia'); +// goog.require('Blockly.Themes.Modern'); diff --git a/core/scrollbar.js b/core/scrollbar.js index c327d8f52..9d20f89dc 100644 --- a/core/scrollbar.js +++ b/core/scrollbar.js @@ -254,7 +254,8 @@ Blockly.Scrollbar.prototype.origin_ = new Blockly.utils.Coordinate(0, 0); * Units are CSS pixels, with (0, 0) at the top left of the browser window. * For a horizontal scrollbar this is the x coordinate of the mouse down event; * for a vertical scrollbar it's the y coordinate of the mouse down event. - * @type {Blockly.utils.Coordinate} + * @type {number} + * @private */ Blockly.Scrollbar.prototype.startDragMouse_ = 0; @@ -305,9 +306,9 @@ if (Blockly.Touch.TOUCH_ENABLED) { } /** - * @param {!Object} first An object containing computed measurements of a + * @param {Object} first An object containing computed measurements of a * workspace. - * @param {!Object} second Another object containing computed measurements of a + * @param {Object} second Another object containing computed measurements of a * workspace. * @return {boolean} Whether the two sets of metrics are equivalent. * @private @@ -359,6 +360,7 @@ Blockly.Scrollbar.prototype.dispose = function() { * Set the length of the scrollbar's handle and change the SVG attribute * accordingly. * @param {number} newLength The new scrollbar handle length in CSS pixels. + * @private */ Blockly.Scrollbar.prototype.setHandleLength_ = function(newLength) { this.handleLength_ = newLength; @@ -625,9 +627,12 @@ Blockly.Scrollbar.prototype.createDom_ = function(opt_class) { 'ry': radius }, this.svgGroup_); - this.workspace_.getThemeManager().subscribe(this.svgHandle_, 'scrollbar', 'fill'); - this.workspace_.getThemeManager().subscribe(this.svgHandle_, 'scrollbarOpacity', 'fill-opacity'); - Blockly.utils.dom.insertAfter(this.outerSvg_, this.workspace_.getParentSvg()); + this.workspace_.getThemeManager().subscribe( + this.svgHandle_, 'scrollbarColour', 'fill'); + this.workspace_.getThemeManager().subscribe( + this.svgHandle_, 'scrollbarOpacity', 'fill-opacity'); + Blockly.utils.dom.insertAfter(this.outerSvg_, + this.workspace_.getParentSvg()); }; /** @@ -709,7 +714,8 @@ Blockly.Scrollbar.prototype.onMouseDownBar_ = function(e) { e.stopPropagation(); return; } - var mouseXY = Blockly.utils.mouseToSvg(e, this.workspace_.getParentSvg(), + var mouseXY = Blockly.utils.mouseToSvg(e, + this.workspace_.getParentSvg(), this.workspace_.getInverseScreenCTM()); var mouseLocation = this.horizontal_ ? mouseXY.x : mouseXY.y; diff --git a/core/theme.js b/core/theme.js index b5528863f..70aa6fba2 100644 --- a/core/theme.js +++ b/core/theme.js @@ -22,9 +22,13 @@ goog.provide('Blockly.Theme'); +goog.require('Blockly.utils'); +goog.require('Blockly.utils.colour'); + /** * Class for a theme. + * @param {string} name Theme name. * @param {!Object.} blockStyles A map from * style names (strings) to objects with style attributes for blocks. * @param {!Object.} categoryStyles A map @@ -34,22 +38,34 @@ goog.provide('Blockly.Theme'); * names to style value. * @constructor */ -Blockly.Theme = function(blockStyles, categoryStyles, opt_componentStyles) { +Blockly.Theme = function(name, blockStyles, categoryStyles, + opt_componentStyles) { + + /** + * The theme name. This can be used to reference a specific theme in CSS. + * @type {string} + * @package + */ + this.name = name; + /** * The block styles map. - * @type {!Object.} + * @type {!Object.} + * @package */ - this.blockStyles_ = blockStyles; + this.blockStyles = blockStyles; /** * The category styles map. * @type {!Object.} + * @package */ - this.categoryStyles_ = categoryStyles; + this.categoryStyles = categoryStyles; /** * The UI components styles map. * @type {!Object.} + * @private */ this.componentStyles_ = opt_componentStyles || Object.create(null); }; @@ -73,62 +89,6 @@ Blockly.Theme.BlockStyle; */ Blockly.Theme.CategoryStyle; -/** - * Overrides or adds all values from blockStyles to blockStyles_ - * @param {Object.} blockStyles Map of - * block styles. - */ -Blockly.Theme.prototype.setAllBlockStyles = function(blockStyles) { - for (var key in blockStyles) { - this.setBlockStyle(key, blockStyles[key]); - } -}; - -/** - * Gets a map of all the block style names. - * @return {!Object.} Map of block styles. - */ -Blockly.Theme.prototype.getAllBlockStyles = function() { - return this.blockStyles_; -}; - -/** - * Gets the BlockStyle for the given block style name. - * @param {string} blockStyleName The name of the block style. - * @return {Blockly.Theme.BlockStyle|undefined} The named block style. - */ -Blockly.Theme.prototype.getBlockStyle = function(blockStyleName) { - return this.blockStyles_[blockStyleName]; -}; - -/** - * Overrides or adds a style to the blockStyles map. - * @param {string} blockStyleName The name of the block style. - * @param {Blockly.Theme.BlockStyle} blockStyle The block style. -*/ -Blockly.Theme.prototype.setBlockStyle = function(blockStyleName, blockStyle) { - this.blockStyles_[blockStyleName] = blockStyle; -}; - -/** - * Gets the CategoryStyle for the given category style name. - * @param {string} categoryStyleName The name of the category style. - * @return {Blockly.Theme.CategoryStyle|undefined} The named category style. - */ -Blockly.Theme.prototype.getCategoryStyle = function(categoryStyleName) { - return this.categoryStyles_[categoryStyleName]; -}; - -/** - * Overrides or adds a style to the categoryStyles map. - * @param {string} categoryStyleName The name of the category style. - * @param {Blockly.Theme.CategoryStyle} categoryStyle The category style. -*/ -Blockly.Theme.prototype.setCategoryStyle = function(categoryStyleName, - categoryStyle) { - this.categoryStyles_[categoryStyleName] = categoryStyle; -}; - /** * Gets the style for a given Blockly UI component. If the style value is a * string, we attempt to find the value of any named references. diff --git a/core/theme/classic.js b/core/theme/classic.js index b84057700..b39836de4 100644 --- a/core/theme/classic.js +++ b/core/theme/classic.js @@ -94,5 +94,5 @@ Blockly.Themes.Classic.categoryStyles = { }; Blockly.Themes.Classic = - new Blockly.Theme(Blockly.Themes.Classic.defaultBlockStyles, + new Blockly.Theme('classic', Blockly.Themes.Classic.defaultBlockStyles, Blockly.Themes.Classic.categoryStyles); diff --git a/core/theme/dark.js b/core/theme/dark.js index 2aff84c02..e9b7697b2 100644 --- a/core/theme/dark.js +++ b/core/theme/dark.js @@ -23,6 +23,7 @@ goog.provide('Blockly.Themes.Dark'); +goog.require('Blockly.Css'); goog.require('Blockly.Theme'); @@ -115,14 +116,50 @@ Blockly.Themes.Dark.categoryStyles = { // This style is still being fleshed out and may change. Blockly.Themes.Dark = - new Blockly.Theme(Blockly.Themes.Dark.defaultBlockStyles, + new Blockly.Theme('dark', Blockly.Themes.Dark.defaultBlockStyles, Blockly.Themes.Dark.categoryStyles); -Blockly.Themes.Dark.setComponentStyle('workspace', '#1e1e1e'); -Blockly.Themes.Dark.setComponentStyle('toolbox', '#333'); -Blockly.Themes.Dark.setComponentStyle('toolboxText', '#fff'); -Blockly.Themes.Dark.setComponentStyle('flyout', '#252526'); -Blockly.Themes.Dark.setComponentStyle('flyoutText', '#ccc'); +Blockly.Themes.Dark.setComponentStyle('workspaceBackgroundColour', '#1e1e1e'); +Blockly.Themes.Dark.setComponentStyle('toolboxBackgroundColour', '#333'); +Blockly.Themes.Dark.setComponentStyle('toolboxForegroundColour', '#fff'); +Blockly.Themes.Dark.setComponentStyle('flyoutBackgroundColour', '#252526'); +Blockly.Themes.Dark.setComponentStyle('flyoutForegroundColour', '#ccc'); Blockly.Themes.Dark.setComponentStyle('flyoutOpacity', 1); -Blockly.Themes.Dark.setComponentStyle('scrollbar', '#797979'); +Blockly.Themes.Dark.setComponentStyle('scrollbarColour', '#797979'); Blockly.Themes.Dark.setComponentStyle('scrollbarOpacity', 0.4); + +/** + * CSS for the dark theme. + * This registers CSS that is specific to this theme. It does so by prepending a + * ``.dark-theme`` selector before every CSS rule that we wish to override by + * this theme. + */ +(function() { + var selector = '.dark-theme'; + Blockly.Css.register([ + /* eslint-disable indent */ + // Toolbox hover + selector + ' .blocklyTreeRow:not(.blocklyTreeSelected):hover {', + 'background-color: #2a2d2e;', + '}', + // Dropdown and Widget div. + selector + '.blocklyWidgetDiv .goog-menu, ', + selector + '.blocklyDropDownDiv {', + 'background-color: #3c3c3c;', + '}', + selector + '.blocklyDropDownDiv {', + 'border-color: #565656;', + '}', + selector + '.blocklyWidgetDiv .goog-menuitem-content, ', + selector + '.blocklyDropDownDiv .goog-menuitem-content {', + 'color: #f0f0f0;', + '}', + selector + '.blocklyWidgetDiv .goog-menuitem-disabled', + ' .goog-menuitem-content,', + selector + '.blocklyDropDownDiv .goog-menuitem-disabled', + ' .goog-menuitem-content {', + 'color: #8a8a8a !important;', + '}', + /* eslint-enable indent */ + ]); +})(); diff --git a/core/theme/deuteranopia.js b/core/theme/deuteranopia.js new file mode 100644 index 000000000..dbd1bfbab --- /dev/null +++ b/core/theme/deuteranopia.js @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Deuteranopia theme. + * A color palette for people that have deuteranopia(the inability to perceive + * green light). This can also be used for people that have protanopia(the + * inability to perceive red light). + */ +'use strict'; + +goog.provide('Blockly.Themes.Deuteranopia'); + +goog.require('Blockly.Theme'); + + +// Temporary holding object. +Blockly.Themes.Deuteranopia = {}; + +Blockly.Themes.Deuteranopia.defaultBlockStyles = { + "colour_blocks": { + "colourPrimary": "#f2a72c", + "colourSecondary": "#f1c172", + "colourTertiary": "#da921c" + }, + "list_blocks": { + "colourPrimary": "#7d65ab", + "colourSecondary": "#a88be0", + "colourTertiary": "#66518e" + }, + "logic_blocks": { + "colourPrimary": "#9fd2f1", + "colourSecondary": "#c0e0f4", + "colourTertiary": "#74bae5" + }, + "loop_blocks": { + "colourPrimary": "#795a07", + "colourSecondary": "#ac8726", + "colourTertiary": "#c4a03f" + }, + "math_blocks": { + "colourPrimary": "#e6da39", + "colourSecondary": "#f3ec8e", + "colourTertiary": "#f2eeb7" + }, + "procedure_blocks": { + "colourPrimary": "#590721", + "colourSecondary": "#8c475d", + "colourTertiary": "#885464" + }, + "text_blocks": { + "colourPrimary": "#058863", + "colourSecondary": "#5ecfaf", + "colourTertiary": "#04684c" + }, + "variable_blocks": { + "colourPrimary": "#47025a", + "colourSecondary": "#820fa1", + "colourTertiary": "#8e579d" + }, + "variable_dynamic_blocks": { + "colourPrimary": "#47025a", + "colourSecondary": "#820fa1", + "colourTertiary": "#8e579d" + } +}; + +Blockly.Themes.Deuteranopia.categoryStyles = { + "colour_category": { + "colour": "#f2a72c" + }, + "list_category": { + "colour": "#7d65ab" + }, + "logic_category": { + "colour": "#9fd2f1" + }, + "loop_category": { + "colour": "#795a07" + }, + "math_category": { + "colour": "#e6da39" + }, + "procedure_category": { + "colour": "#590721" + }, + "text_category": { + "colour": "#058863" + }, + "variable_category": { + "colour": "#47025a" + }, + "variable_dynamic_category": { + "colour": "#47025a" + } +}; + +Blockly.Themes.Deuteranopia = + new Blockly.Theme('deuteranopia', + Blockly.Themes.Deuteranopia.defaultBlockStyles, + Blockly.Themes.Deuteranopia.categoryStyles); diff --git a/core/theme/highcontrast.js b/core/theme/highcontrast.js index 449619a6d..3f2c15850 100644 --- a/core/theme/highcontrast.js +++ b/core/theme/highcontrast.js @@ -115,5 +115,6 @@ Blockly.Themes.HighContrast.categoryStyles = { // This style is still being fleshed out and may change. Blockly.Themes.HighContrast = - new Blockly.Theme(Blockly.Themes.HighContrast.defaultBlockStyles, + new Blockly.Theme('highcontrast', + Blockly.Themes.HighContrast.defaultBlockStyles, Blockly.Themes.HighContrast.categoryStyles); diff --git a/core/theme/modern.js b/core/theme/modern.js index 09e892dba..9d6cc6afd 100644 --- a/core/theme/modern.js +++ b/core/theme/modern.js @@ -115,5 +115,5 @@ Blockly.Themes.Modern.categoryStyles = { // This style is still being fleshed out and may change. Blockly.Themes.Modern = - new Blockly.Theme(Blockly.Themes.Modern.defaultBlockStyles, + new Blockly.Theme('modern', Blockly.Themes.Modern.defaultBlockStyles, Blockly.Themes.Modern.categoryStyles); diff --git a/core/theme/tritanopia.js b/core/theme/tritanopia.js new file mode 100644 index 000000000..d90d86aa0 --- /dev/null +++ b/core/theme/tritanopia.js @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tritanopia theme. + * A color palette for people that have tritanopia(the inability to perceive + * blue light). + */ +'use strict'; + +goog.provide('Blockly.Themes.Tritanopia'); + +goog.require('Blockly.Theme'); + + +// Temporary holding object. +Blockly.Themes.Tritanopia = {}; + +Blockly.Themes.Tritanopia.defaultBlockStyles = { + "colour_blocks": { + "colourPrimary": "#05427f", + "colourSecondary": "#2974c0", + "colourTertiary": "#2d74bb" + }, + "list_blocks": { + "colourPrimary": "#b69ce8", + "colourSecondary": "#ccbaef", + "colourTertiary": "#9176c5" + }, + "logic_blocks": { + "colourPrimary": "#9fd2f1", + "colourSecondary": "#c0e0f4", + "colourTertiary": "#74bae5" + }, + "loop_blocks": { + "colourPrimary": "#aa1846", + "colourSecondary": "#d36185", + "colourTertiary": "#7c1636" + }, + "math_blocks": { + "colourPrimary": "#e6da39", + "colourSecondary": "#f3ec8e", + "colourTertiary": "#f2eeb7" + }, + "procedure_blocks": { + "colourPrimary": "#590721", + "colourSecondary": "#8c475d", + "colourTertiary": "#885464" + }, + "text_blocks": { + "colourPrimary": "#058863", + "colourSecondary": "#5ecfaf", + "colourTertiary": "#04684c" + }, + "variable_blocks": { + "colourPrimary": "#4b2d84", + "colourSecondary": "#816ea7", + "colourTertiary": "#83759e" + }, + "variable_dynamic_blocks": { + "colourPrimary": "#4b2d84", + "colourSecondary": "#816ea7", + "colourTertiary": "#83759e" + } +}; + +Blockly.Themes.Tritanopia.categoryStyles = { + "colour_category": { + "colour": "#05427f" + }, + "list_category": { + "colour": "#b69ce8" + }, + "logic_category": { + "colour": "#9fd2f1" + }, + "loop_category": { + "colour": "#aa1846" + }, + "math_category": { + "colour": "#e6da39" + }, + "procedure_category": { + "colour": "#590721" + }, + "text_category": { + "colour": "#058863" + }, + "variable_category": { + "colour": "#4b2d84" + }, + "variable_dynamic_category": { + "colour": "#4b2d84" + } +}; + +Blockly.Themes.Tritanopia = + new Blockly.Theme('tritanopia', + Blockly.Themes.Tritanopia.defaultBlockStyles, + Blockly.Themes.Tritanopia.categoryStyles); diff --git a/core/theme/zelos.js b/core/theme/zelos.js new file mode 100644 index 000000000..c6305ee62 --- /dev/null +++ b/core/theme/zelos.js @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Zelos theme. + */ +'use strict'; + +goog.provide('Blockly.Themes.Zelos'); + +goog.require('Blockly.Theme'); + + +// Temporary holding object. +Blockly.Themes.Zelos = {}; + +Blockly.Themes.Zelos.defaultBlockStyles = { + "colour_blocks": { + "colourPrimary": "#CF63CF", + "colourSecondary": "#C94FC9", + "colourTertiary": "#BD42BD" + }, + "list_blocks": { + "colourPrimary": "#9966FF", + "colourSecondary": "#855CD6", + "colourTertiary": "#774DCB" + }, + "logic_blocks": { + "colourPrimary": "#4C97FF", + "colourSecondary": "#4280D7", + "colourTertiary": "#3373CC" + }, + "loop_blocks": { + "colourPrimary": "#0fBD8C", + "colourSecondary": "#0DA57A", + "colourTertiary": "#0B8E69" + }, + "math_blocks": { + "colourPrimary": "#59C059", + "colourSecondary": "#46B946", + "colourTertiary": "#389438" + }, + "procedure_blocks": { + "colourPrimary": "#FF6680", + "colourSecondary": "#FF4D6A", + "colourTertiary": "#FF3355" + }, + "text_blocks": { + "colourPrimary": "#FFBF00", + "colourSecondary": "#E6AC00", + "colourTertiary": "#CC9900" + }, + "variable_blocks": { + "colourPrimary": "#FF8C1A", + "colourSecondary": "#FF8000", + "colourTertiary": "#DB6E00" + }, + "variable_dynamic_blocks": { + "colourPrimary": "#FF8C1A", + "colourSecondary": "#FF8000", + "colourTertiary": "#DB6E00" + }, + "hat_blocks": { + "colourPrimary": "#4C97FF", + "colourSecondary": "#4280D7", + "colourTertiary": "#3373CC", + "hat": "cap" + } +}; + +Blockly.Themes.Zelos.categoryStyles = { + "colour_category": { + "colour": "#CF63CF" + }, + "list_category": { + "colour": "#9966FF" + }, + "logic_category": { + "colour": "#4C97FF" + }, + "loop_category": { + "colour": "#0fBD8C" + }, + "math_category": { + "colour": "#59C059" + }, + "procedure_category": { + "colour": "#FF6680" + }, + "text_category": { + "colour": "#FFBF00" + }, + "variable_category": { + "colour": "#FF8C1A" + }, + "variable_dynamic_category": { + "colour": "#FF8C1A" + } +}; + +Blockly.Themes.Zelos = + new Blockly.Theme('zelos', Blockly.Themes.Zelos.defaultBlockStyles, + Blockly.Themes.Zelos.categoryStyles); diff --git a/core/theme_manager.js b/core/theme_manager.js index d53e9cba6..48003911e 100644 --- a/core/theme_manager.js +++ b/core/theme_manager.js @@ -30,11 +30,19 @@ goog.require('Blockly.Theme'); /** * Class for storing and updating a workspace's theme and UI components. + * @param {!Blockly.WorkspaceSvg} workspace The main workspace. * @param {!Blockly.Theme} theme The workspace theme. * @constructor * @package */ -Blockly.ThemeManager = function(theme) { +Blockly.ThemeManager = function(workspace, theme) { + + /** + * The main workspace. + * @type {!Blockly.WorkspaceSvg} + * @private + */ + this.workspace_ = workspace; /** * The Blockly theme to use. @@ -87,8 +95,18 @@ Blockly.ThemeManager.prototype.setTheme = function(theme) { return; } + var prevTheme = this.theme_; this.theme_ = theme; + // Set the theme name onto the injection div. + var injectionDiv = this.workspace_.getInjectionDiv(); + if (injectionDiv) { + if (prevTheme) { + Blockly.utils.dom.removeClass(injectionDiv, prevTheme.name + '-theme'); + } + Blockly.utils.dom.addClass(injectionDiv, this.theme_.name + '-theme'); + } + // Refresh all subscribed workspaces. for (var i = 0, workspace; (workspace = this.subscribedWorkspaces_[i]); i++) { workspace.refreshTheme(); @@ -96,7 +114,7 @@ Blockly.ThemeManager.prototype.setTheme = function(theme) { // Refresh all registered Blockly UI components. for (var i = 0, keys = Object.keys(this.componentDB_), - key; key = keys[i]; i++) { + key; (key = keys[i]); i++) { for (var j = 0, component; (component = this.componentDB_[key][j]); j++) { var element = component.element; var propertyName = component.propertyName; @@ -104,6 +122,8 @@ Blockly.ThemeManager.prototype.setTheme = function(theme) { element.style[propertyName] = style || ''; } } + + Blockly.hideChaff(); }; /** diff --git a/core/toolbox.js b/core/toolbox.js index 0d72efc78..c0a82d8ea 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -110,6 +110,13 @@ Blockly.Toolbox = function(workspace) { 'blocklyHorizontalTreeRtl' : 'blocklyHorizontalTree'); this.config_['cssTreeIcon'] = ''; } + + /** + * The toolbox flyout. + * @type {Blockly.Flyout} + * @private + */ + this.flyout_ = null; }; /** @@ -140,6 +147,8 @@ Blockly.Toolbox.prototype.lastCategory_ = null; /** * Initializes the toolbox. + * @throws {Error} If missing a require for both `Blockly.HorizontalFlyout` and + * `Blockly.VerticalFlyout`. */ Blockly.Toolbox.prototype.init = function() { var workspace = this.workspace_; @@ -154,8 +163,9 @@ Blockly.Toolbox.prototype.init = function() { this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR'); svg.parentNode.insertBefore(this.HtmlDiv, svg); var themeManager = workspace.getThemeManager(); - themeManager.subscribe(this.HtmlDiv, 'toolbox', 'background-color'); - themeManager.subscribe(this.HtmlDiv, 'toolboxText', 'color'); + themeManager.subscribe(this.HtmlDiv, 'toolboxBackgroundColour', + 'background-color'); + themeManager.subscribe(this.HtmlDiv, 'toolboxForegroundColour', 'color'); // Clicking on toolbox closes popups. Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this, @@ -169,20 +179,17 @@ Blockly.Toolbox.prototype.init = function() { } Blockly.Touch.clearTouchIdentifier(); // Don't block future drags. }, /* opt_noCaptureIdentifier */ false, /* opt_noPreventDefault */ true); - var workspaceOptions = { - disabledPatternId: workspace.options.disabledPatternId, - parentWorkspace: workspace, - RTL: workspace.RTL, - oneBasedIndex: workspace.options.oneBasedIndex, - horizontalLayout: workspace.horizontalLayout, - toolboxPosition: workspace.options.toolboxPosition, - renderer: workspace.options.renderer - }; - /** - * @type {!Blockly.Flyout} - * @private - */ - this.flyout_ = null; + var workspaceOptions = new Blockly.Options( + /** @type {!Blockly.BlocklyOptions} */ + ({ + 'parentWorkspace': workspace, + 'rtl': workspace.RTL, + 'oneBasedIndex': workspace.options.oneBasedIndex, + 'horizontalLayout': workspace.horizontalLayout, + 'renderer': workspace.options.renderer + })); + workspaceOptions.toolboxPosition = workspace.options.toolboxPosition; + if (workspace.horizontalLayout) { if (!Blockly.HorizontalFlyout) { throw Error('Missing require for Blockly.HorizontalFlyout'); @@ -194,9 +201,13 @@ Blockly.Toolbox.prototype.init = function() { } this.flyout_ = new Blockly.VerticalFlyout(workspaceOptions); } + if (!this.flyout_) { + 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'), - this.workspace_.getParentSvg()); + Blockly.utils.dom.insertAfter(this.flyout_.createDom('svg'), svg); this.flyout_.init(workspace); this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif'; @@ -207,7 +218,7 @@ Blockly.Toolbox.prototype.init = function() { /** * Fill the toolbox with categories and blocks. - * @param {!Node} languageTree DOM tree of blocks. + * @param {Node} languageTree DOM tree of blocks. * @package */ Blockly.Toolbox.prototype.renderTree = function(languageTree) { @@ -225,8 +236,8 @@ Blockly.Toolbox.prototype.renderTree = function(languageTree) { if (languageTree) { this.tree_.blocks = []; this.hasColours_ = false; - var openNode = - this.syncTrees_(languageTree, this.tree_, this.workspace_.options.pathToMedia); + openNode = this.syncTrees_( + languageTree, this.tree_, this.workspace_.options.pathToMedia); if (this.tree_.blocks.length) { throw Error('Toolbox cannot have both blocks and categories ' + @@ -245,7 +256,8 @@ Blockly.Toolbox.prototype.renderTree = function(languageTree) { // Trees have an implicit orientation of vertical, so we only need to set this // when the toolbox is in horizontal mode. if (this.horizontalLayout_) { - Blockly.utils.aria.setState(/** @type {!Element} */ (this.tree_.getElement()), + Blockly.utils.aria.setState( + /** @type {!Element} */ (this.tree_.getElement()), Blockly.utils.aria.State.ORIENTATION, 'horizontal'); } }; @@ -287,13 +299,13 @@ Blockly.Toolbox.prototype.handleAfterTreeSelected_ = function( if (this.lastCategory_ != newNode) { this.flyout_.scrollToStart(); } - if (Blockly.keyboardAccessibilityMode) { + if (this.workspace_.keyboardAccessibilityMode) { Blockly.navigation.setState(Blockly.navigation.STATE_TOOLBOX); } } else { // Hide the flyout. this.flyout_.hide(); - if (Blockly.keyboardAccessibilityMode && + if (this.workspace_.keyboardAccessibilityMode && !(newNode instanceof Blockly.Toolbox.TreeSeparator)) { Blockly.navigation.setState(Blockly.navigation.STATE_WS); } @@ -353,7 +365,6 @@ Blockly.Toolbox.prototype.dispose = function() { this.tree_.dispose(); this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv); Blockly.utils.dom.removeNode(this.HtmlDiv); - this.workspace_ = null; this.lastCategory_ = null; }; @@ -373,6 +384,14 @@ Blockly.Toolbox.prototype.getHeight = function() { return this.height; }; +/** + * Get the toolbox flyout. + * @return {Blockly.Flyout} The toolbox flyout. + */ +Blockly.Toolbox.prototype.getFlyout = function() { + return this.flyout_; +}; + /** * Move the toolbox to the edge. */ @@ -382,8 +401,7 @@ Blockly.Toolbox.prototype.position = function() { // Not initialized yet. return; } - var svg = this.workspace_.getParentSvg(); - var svgSize = Blockly.svgSize(svg); + var svgSize = Blockly.svgSize(this.workspace_.getParentSvg()); if (this.horizontalLayout_) { treeDiv.style.left = '0'; treeDiv.style.height = 'auto'; @@ -418,7 +436,7 @@ Blockly.Toolbox.prototype.position = function() { Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { var openNode = null; var lastElement = null; - for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) { + for (var i = 0, childIn; (childIn = treeIn.childNodes[i]); i++) { if (!childIn.tagName) { // Skip over text. continue; @@ -474,7 +492,8 @@ Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { // Separator between two categories. // treeOut.add(new Blockly.Toolbox.TreeSeparator( - /** @type {!Blockly.tree.BaseNode.Config} */ (this.treeSeparatorConfig_))); + /** @type {!Blockly.tree.BaseNode.Config} */ + (this.treeSeparatorConfig_))); break; } // Otherwise falls through. @@ -531,13 +550,14 @@ Blockly.Toolbox.prototype.setColour_ = function(colourValue, childOut, * @param {string} styleName Name of the style. * @param {!Blockly.tree.TreeNode} childOut The child to set the hexColour on. * @param {string} categoryName Name of the toolbox category. + * @private */ Blockly.Toolbox.prototype.setColourFromStyle_ = function( styleName, childOut, categoryName) { childOut.styleName = styleName; var theme = this.workspace_.getTheme(); if (styleName && theme) { - var style = theme.getCategoryStyle(styleName); + var style = theme.categoryStyles[styleName]; if (style && style.colour) { this.setColour_(style.colour, childOut, categoryName); } else { @@ -557,7 +577,7 @@ Blockly.Toolbox.prototype.updateColourFromTheme_ = function(opt_tree) { var tree = opt_tree || this.tree_; if (tree) { var children = tree.getChildren(false); - for (var i = 0, child; child = children[i]; i++) { + for (var i = 0, child; (child = children[i]); i++) { if (child.styleName) { this.setColourFromStyle_(child.styleName, child, ''); this.addColour_(); @@ -604,7 +624,7 @@ Blockly.Toolbox.prototype.updateSelectedItemColour_ = function(tree) { Blockly.Toolbox.prototype.addColour_ = function(opt_tree) { var tree = opt_tree || this.tree_; var children = tree.getChildren(false); - for (var i = 0, child; child = children[i]; i++) { + for (var i = 0, child; (child = children[i]); i++) { var element = child.getRowElement(); if (element) { if (this.hasColours_) { diff --git a/core/tooltip.js b/core/tooltip.js index 747bd3f18..28ddefeb8 100644 --- a/core/tooltip.js +++ b/core/tooltip.js @@ -78,6 +78,7 @@ Blockly.Tooltip.lastY_ = 0; /** * Current element being pointed at. + * @type {Element} * @private */ Blockly.Tooltip.element_ = null; @@ -85,6 +86,7 @@ Blockly.Tooltip.element_ = null; /** * Once a tooltip has opened for an element, that element is 'poisoned' and * cannot respawn a tooltip until the pointer moves over a different element. + * @type {Element} * @private */ Blockly.Tooltip.poisonedElement_ = null; diff --git a/core/touch.js b/core/touch.js index 7e6d097ea..2c5094ae6 100644 --- a/core/touch.js +++ b/core/touch.js @@ -92,9 +92,9 @@ Blockly.longPid_ = 0; * if the touch event terminates early. * @param {!Event} e Touch start event. * @param {Blockly.Gesture} gesture The gesture that triggered this longStart. - * @private + * @package */ -Blockly.longStart_ = function(e, gesture) { +Blockly.longStart = function(e, gesture) { Blockly.longStop_(); // Punt on multitouch events. if (e.changedTouches && e.changedTouches.length != 1) { diff --git a/core/touch_gesture.js b/core/touch_gesture.js index 309ff9744..374fdddb5 100644 --- a/core/touch_gesture.js +++ b/core/touch_gesture.js @@ -55,10 +55,10 @@ Blockly.TouchGesture = function(e, creatorWorkspace) { /** * A map of cached points used for tracking multi-touch gestures. - * @type {Object} + * @type {!Object} * @private */ - this.cachedPoints_ = {}; + this.cachedPoints_ = Object.create(null); /** * This is the ratio between the starting distance between the touch points @@ -81,10 +81,17 @@ Blockly.TouchGesture = function(e, creatorWorkspace) { * A handle to use to unbind the second touch start or pointer down listener * at the end of a drag. * Opaque data returned from Blockly.bindEventWithChecks_. - * @type {Array.} + * @type {?Blockly.EventData} * @private */ this.onStartWrapper_ = null; + + /** + * Boolean for whether or not the workspace supports pinch-zoom. + * @type {?boolean} + * @private + */ + this.isPinchZoomEnabled_ = null; }; Blockly.utils.object.inherits(Blockly.TouchGesture, Blockly.Gesture); @@ -107,6 +114,8 @@ Blockly.TouchGesture.ZOOM_OUT_MULTIPLIER = 6; * @package */ Blockly.TouchGesture.prototype.doStart = function(e) { + this.isPinchZoomEnabled_ = this.startWorkspace_.options.zoomOptions && + this.startWorkspace_.options.zoomOptions.pinch; Blockly.TouchGesture.superClass_.doStart.call(this, e); if (!this.isEnding_ && Blockly.Touch.isTouchEvent(e)) { this.handleTouchStart(e); @@ -233,10 +242,12 @@ Blockly.TouchGesture.prototype.handleTouchStart = function(e) { // store the pointerId in the current list of pointers this.cachedPoints_[pointerId] = this.getTouchPoint(e); var pointers = Object.keys(this.cachedPoints_); - // If two pointers are down, check for pinch gestures + // If two pointers are down, store info if (pointers.length == 2) { - var point0 = this.cachedPoints_[pointers[0]]; - var point1 = this.cachedPoints_[pointers[1]]; + var point0 = /** @type {!Blockly.utils.Coordinate} */ ( + this.cachedPoints_[pointers[0]]); + var point1 = /** @type {!Blockly.utils.Coordinate} */ ( + this.cachedPoints_[pointers[1]]); this.startDistance_ = Blockly.utils.Coordinate.distance(point0, point1); this.isMultiTouch_ = true; e.preventDefault(); @@ -255,30 +266,43 @@ Blockly.TouchGesture.prototype.handleTouchMove = function(e) { this.cachedPoints_[pointerId] = this.getTouchPoint(e); var pointers = Object.keys(this.cachedPoints_); - // If two pointers are down, check for pinch gestures - if (pointers.length == 2) { - // Calculate the distance between the two pointers - var point0 = this.cachedPoints_[pointers[0]]; - var point1 = this.cachedPoints_[pointers[1]]; - var moveDistance = Blockly.utils.Coordinate.distance(point0, point1); - var startDistance = this.startDistance_; - var scale = this.touchScale_ = moveDistance / startDistance; - - if (this.previousScale_ > 0 && this.previousScale_ < Infinity) { - var gestureScale = scale - this.previousScale_; - var delta = gestureScale > 0 ? - gestureScale * Blockly.TouchGesture.ZOOM_IN_MULTIPLIER : - gestureScale * Blockly.TouchGesture.ZOOM_OUT_MULTIPLIER; - var workspace = this.startWorkspace_; - var position = Blockly.utils.mouseToSvg( - e, workspace.getParentSvg(), workspace.getInverseScreenCTM()); - workspace.zoom(position.x, position.y, delta); - } - this.previousScale_ = scale; - e.preventDefault(); + if (this.isPinchZoomEnabled_ && pointers.length === 2) { + this.handlePinch_(e); + } else { + Blockly.TouchGesture.superClass_.handleMove.call(this, e); } }; +/** +* Handle pinch zoom gesture. +* @param {!Event} e A touch move, or pointer move event. +* @private +*/ +Blockly.TouchGesture.prototype.handlePinch_ = function(e) { + var pointers = Object.keys(this.cachedPoints_); + // Calculate the distance between the two pointers + var point0 = /** @type {!Blockly.utils.Coordinate} */ ( + this.cachedPoints_[pointers[0]]); + var point1 = /** @type {!Blockly.utils.Coordinate} */ ( + this.cachedPoints_[pointers[1]]); + var moveDistance = Blockly.utils.Coordinate.distance(point0, point1); + var scale = moveDistance / this.startDistance_; + + if (this.previousScale_ > 0 && this.previousScale_ < Infinity) { + var gestureScale = scale - this.previousScale_; + var delta = gestureScale > 0 ? + gestureScale * Blockly.TouchGesture.ZOOM_IN_MULTIPLIER : + gestureScale * Blockly.TouchGesture.ZOOM_OUT_MULTIPLIER; + var workspace = this.startWorkspace_; + var position = Blockly.utils.mouseToSvg( + e, workspace.getParentSvg(), workspace.getInverseScreenCTM()); + workspace.zoom(position.x, position.y, delta); + } + this.previousScale_ = scale; + e.preventDefault(); +}; + + /** * Handle a touch end or pointer end event and end the gesture. * @param {!Event} e A touch end, or pointer end event. @@ -290,7 +314,7 @@ Blockly.TouchGesture.prototype.handleTouchEnd = function(e) { delete this.cachedPoints_[pointerId]; } if (Object.keys(this.cachedPoints_).length < 2) { - this.cachedPoints_ = {}; + this.cachedPoints_ = Object.create(null); this.previousScale_ = 0; } }; diff --git a/core/trashcan.js b/core/trashcan.js index 7b5d676d5..fde748787 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -31,13 +31,13 @@ goog.require('Blockly.Xml'); /** * Class for a trash can. - * @param {!Blockly.Workspace} workspace The workspace to sit in. + * @param {!Blockly.WorkspaceSvg} workspace The workspace to sit in. * @constructor */ Blockly.Trashcan = function(workspace) { /** * The workspace the trashcan sits in. - * @type {!Blockly.Workspace} + * @type {!Blockly.WorkspaceSvg} * @private */ this.workspace_ = workspace; @@ -49,19 +49,26 @@ Blockly.Trashcan = function(workspace) { */ this.contents_ = []; + /** + * The trashcan flyout. + * @type {Blockly.Flyout} + * @package + */ + this.flyout = null; if (this.workspace_.options.maxTrashcanContents <= 0) { return; } // Create flyout options. - var flyoutWorkspaceOptions = { - scrollbars: true, - disabledPatternId: this.workspace_.options.disabledPatternId, - parentWorkspace: this.workspace_, - RTL: this.workspace_.RTL, - oneBasedIndex: this.workspace_.options.oneBasedIndex, - renderer: this.workspace_.options.renderer - }; + var flyoutWorkspaceOptions = new Blockly.Options( + /** @type {!Blockly.BlocklyOptions} */ + ({ + 'scrollbars': true, + 'parentWorkspace': this.workspace_, + 'rtl': this.workspace_.RTL, + 'oneBasedIndex': this.workspace_.options.oneBasedIndex, + 'renderer': this.workspace_.options.renderer + })); // Create vertical or horizontal flyout. if (this.workspace_.horizontalLayout) { flyoutWorkspaceOptions.toolboxPosition = @@ -70,7 +77,7 @@ Blockly.Trashcan = function(workspace) { if (!Blockly.HorizontalFlyout) { throw Error('Missing require for Blockly.HorizontalFlyout'); } - this.flyout_ = new Blockly.HorizontalFlyout(flyoutWorkspaceOptions); + this.flyout = new Blockly.HorizontalFlyout(flyoutWorkspaceOptions); } else { flyoutWorkspaceOptions.toolboxPosition = this.workspace_.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT ? @@ -78,7 +85,7 @@ Blockly.Trashcan = function(workspace) { if (!Blockly.VerticalFlyout) { throw Error('Missing require for Blockly.VerticalFlyout'); } - this.flyout_ = new Blockly.VerticalFlyout(flyoutWorkspaceOptions); + this.flyout = new Blockly.VerticalFlyout(flyoutWorkspaceOptions); } this.workspace_.addChangeListener(this.onDelete_.bind(this)); }; @@ -284,24 +291,20 @@ Blockly.Trashcan.prototype.createDom = function() { */ Blockly.Trashcan.prototype.init = function(verticalSpacing) { if (this.workspace_.options.maxTrashcanContents > 0) { - Blockly.utils.dom.insertAfter(this.flyout_.createDom('svg'), + Blockly.utils.dom.insertAfter(this.flyout.createDom('svg'), this.workspace_.getParentSvg()); - this.flyout_.init(this.workspace_); - this.flyout_.isBlockCreatable_ = function() { - // All blocks, including disabled ones, can be dragged from the - // trashcan flyout. - return true; - }; + this.flyout.init(this.workspace_); } this.verticalSpacing_ = this.MARGIN_BOTTOM_ + verticalSpacing; - this.setOpen_(false); + this.setOpen(false); return this.verticalSpacing_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_; }; /** * Dispose of this trash can. * Unlink from all DOM elements to prevent memory leaks. + * @suppress {checkTypes} */ Blockly.Trashcan.prototype.dispose = function() { if (this.svgGroup_) { @@ -370,9 +373,9 @@ Blockly.Trashcan.prototype.getClientRect = function() { /** * Flip the lid open or shut. * @param {boolean} state True if open. - * @private + * @package */ -Blockly.Trashcan.prototype.setOpen_ = function(state) { +Blockly.Trashcan.prototype.setOpen = function(state) { if (this.isOpen == state) { return; } @@ -416,7 +419,7 @@ Blockly.Trashcan.prototype.setLidAngle_ = function(lidAngle) { * Called externally after a drag. */ Blockly.Trashcan.prototype.close = function() { - this.setOpen_(false); + this.setOpen(false); }; /** @@ -428,10 +431,10 @@ Blockly.Trashcan.prototype.click = function() { } var xml = []; - for (var i = 0, text; text = this.contents_[i]; i++) { + for (var i = 0, text; (text = this.contents_[i]); i++) { xml[i] = Blockly.Xml.textToDom(text); } - this.flyout_.show(xml); + this.flyout.show(xml); }; /** @@ -440,7 +443,7 @@ Blockly.Trashcan.prototype.click = function() { */ Blockly.Trashcan.prototype.mouseOver_ = function() { if (this.contents_.length) { - this.setOpen_(true); + this.setOpen(true); } }; @@ -451,8 +454,8 @@ Blockly.Trashcan.prototype.mouseOver_ = function() { */ Blockly.Trashcan.prototype.mouseOut_ = function() { // No need to do a .hasBlocks check here because if it doesn't the trashcan - // wont be open in the first place, and setOpen_ won't run. - this.setOpen_(false); + // won't be open in the first place, and setOpen won't run. + this.setOpen(false); }; /** @@ -501,6 +504,12 @@ Blockly.Trashcan.prototype.cleanBlockXML_ = function(xml) { node.removeAttribute('x'); node.removeAttribute('y'); node.removeAttribute('id'); + node.removeAttribute('disabled'); + if (node.nodeName == 'comment') { // Future proof just in case. + node.removeAttribute('h'); + node.removeAttribute('w'); + node.removeAttribute('pinned'); + } } // Try to go down the tree diff --git a/core/ui_menu_utils.js b/core/ui_menu_utils.js index 35907e381..bd77bf750 100644 --- a/core/ui_menu_utils.js +++ b/core/ui_menu_utils.js @@ -39,7 +39,7 @@ goog.require('Blockly.utils.style'); */ Blockly.utils.uiMenu.getSize = function(menu) { var menuDom = menu.getElement(); - var menuSize = Blockly.utils.style.getSize(menuDom); + 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/utils.js b/core/utils.js index 48f91e64e..2bacaee0d 100644 --- a/core/utils.js +++ b/core/utils.js @@ -30,6 +30,8 @@ goog.provide('Blockly.utils'); goog.require('Blockly.Msg'); +goog.require('Blockly.constants'); +goog.require('Blockly.utils.colour'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.global'); goog.require('Blockly.utils.string'); @@ -121,7 +123,7 @@ Blockly.utils.getInjectionDivXY_ = function(element) { if ((' ' + classes + ' ').indexOf(' injectionDiv ') != -1) { break; } - element = element.parentNode; + element = /** @type {!Element} */ (element.parentNode); } return new Blockly.utils.Coordinate(x, y); }; @@ -190,6 +192,7 @@ Blockly.utils.mouseToSvg = function(e, svg, matrix) { Blockly.utils.getScrollDeltaPixels = function(e) { switch (e.deltaMode) { case 0x00: // Pixel mode. + default: return { x: e.deltaX, y: e.deltaY @@ -236,7 +239,7 @@ Blockly.utils.replaceMessageReferences = function(message) { var interpolatedResult = Blockly.utils.tokenizeInterpolation_(message, false); // When parseInterpolationTokens == false, interpolatedResult should be at // most length 1. - return interpolatedResult.length ? interpolatedResult[0] : ''; + return interpolatedResult.length ? String(interpolatedResult[0]) : ''; }; /** @@ -573,7 +576,7 @@ Blockly.utils.getBlockTypeCounts = function(block, opt_stripFollowing) { descendants.splice(index, descendants.length - index); } } - for (var i = 0, checkBlock; checkBlock = descendants[i]; i++) { + for (var i = 0, checkBlock; (checkBlock = descendants[i]); i++) { if (typeCountsMap[checkBlock.type]) { typeCountsMap[checkBlock.type]++; } else { @@ -618,3 +621,41 @@ Blockly.utils.screenToWsCoordinates = function(ws, screenCoordinates) { var finalOffsetMainWs = finalOffsetPixels.scale(1 / ws.scale); return finalOffsetMainWs; }; + +/** + * Parse a block colour from a number or string, as provided in a block + * definition. + * @param {number|string} colour HSV hue value (0 to 360), #RRGGBB string, + * or a message reference string pointing to one of those two values. + * @return {{hue: ?number, hex: string}} An object containing the colour as + * a #RRGGBB string, and the hue if the input was an HSV hue value. + * @throws {Error} If the colour cannot be parsed. + */ +Blockly.utils.parseBlockColour = function(colour) { + var dereferenced = (typeof colour == 'string') ? + Blockly.utils.replaceMessageReferences(colour) : colour; + + var hue = Number(dereferenced); + if (!isNaN(hue) && 0 <= hue && hue <= 360) { + return { + hue: hue, + hex: Blockly.utils.colour.hsvToHex(hue, Blockly.HSV_SATURATION, + Blockly.HSV_VALUE * 255) + }; + } else { + var hex = Blockly.utils.colour.parse(dereferenced); + if (hex) { + // Only store hue if colour is set as a hue. + return { + hue: null, + hex: hex + }; + } else { + var errorMsg = 'Invalid colour: "' + dereferenced + '"'; + if (colour != dereferenced) { + errorMsg += ' (from "' + colour + '")'; + } + throw Error(errorMsg); + } + } +}; diff --git a/core/utils/aria.js b/core/utils/aria.js index 14f31beb2..3ba1eac11 100644 --- a/core/utils/aria.js +++ b/core/utils/aria.js @@ -44,55 +44,6 @@ Blockly.utils.aria.ROLE_ATTRIBUTE_ = 'role'; * @enum {string} */ Blockly.utils.aria.Role = { - // ARIA role for an alert element that doesn't need to be explicitly closed. - ALERT: 'alert', - - // ARIA role for an alert dialog element that takes focus and must be closed. - ALERTDIALOG: 'alertdialog', - - // ARIA role for an application that implements its own keyboard navigation. - APPLICATION: 'application', - - // ARIA role for an article. - ARTICLE: 'article', - - // ARIA role for a banner containing mostly site content, not page content. - BANNER: 'banner', - - // ARIA role for a button element. - BUTTON: 'button', - - // ARIA role for a checkbox button element; use with the CHECKED state. - CHECKBOX: 'checkbox', - - // ARIA role for a column header of a table or grid. - COLUMNHEADER: 'columnheader', - - // ARIA role for a combo box element. - COMBOBOX: 'combobox', - - // ARIA role for a supporting section of the document. - COMPLEMENTARY: 'complementary', - - // ARIA role for a large perceivable region that contains information - // about the parent document. - CONTENTINFO: 'contentinfo', - - // ARIA role for a definition of a term or concept. - DEFINITION: 'definition', - - // ARIA role for a dialog, some descendant must take initial focus. - DIALOG: 'dialog', - - // ARIA role for a directory, like a table of contents. - DIRECTORY: 'directory', - - // ARIA role for a part of a page that's a document, not a web application. - DOCUMENT: 'document', - - // ARIA role for a landmark region logically considered one form. - FORM: 'form', - // ARIA role for an interactive control of tabular data. GRID: 'grid', @@ -102,57 +53,18 @@ Blockly.utils.aria.Role = { // ARIA role for a group of related elements like tree item siblings. GROUP: 'group', - // ARIA role for a heading element. - HEADING: 'heading', - - // ARIA role for a container of elements that together comprise one image. - IMG: 'img', - - // ARIA role for a link. - LINK: 'link', - - // ARIA role for a list of non-interactive list items. - LIST: 'list', - // ARIA role for a listbox. LISTBOX: 'listbox', - // ARIA role for a list item. - LISTITEM: 'listitem', - - // ARIA role for a live region where new information is added. - LOG: 'log', - - // ARIA landmark role for the main content in a document. Use only once. - MAIN: 'main', - - // ARIA role for a live region of non-essential information that changes. - MARQUEE: 'marquee', - - // ARIA role for a mathematical expression. - MATH: 'math', - // ARIA role for a popup menu. MENU: 'menu', - // ARIA role for a menubar element containing menu elements. - MENUBAR: 'menubar', - // ARIA role for menu item elements. MENUITEM: 'menuitem', // ARIA role for a checkbox box element inside a menu. MENUITEMCHECKBOX: 'menuitemcheckbox', - // ARIA role for a radio button element inside a menu. - MENUITEMRADIO: 'menuitemradio', - - // ARIA landmark role for a collection of navigation links. - NAVIGATION: 'navigation', - - // ARIA role for a section ancillary to the main content. - NOTE: 'note', - // ARIA role for option items that are children of combobox, listbox, menu, // radiogroup, or tree elements. OPTION: 'option', @@ -160,78 +72,12 @@ Blockly.utils.aria.Role = { // ARIA role for ignorable cosmetic elements with no semantic significance. PRESENTATION: 'presentation', - // ARIA role for a progress bar element. - PROGRESSBAR: 'progressbar', - - // ARIA role for a radio button element. - RADIO: 'radio', - - // ARIA role for a group of connected radio button elements. - RADIOGROUP: 'radiogroup', - - // ARIA role for an important region of the page. - REGION: 'region', - // ARIA role for a row of cells in a grid. ROW: 'row', - // ARIA role for a group of one or more rows in a grid. - ROWGROUP: 'rowgroup', - - // ARIA role for a row header of a table or grid. - ROWHEADER: 'rowheader', - - // ARIA role for a scrollbar element. - SCROLLBAR: 'scrollbar', - - // ARIA landmark role for a part of the page providing search functionality. - SEARCH: 'search', - - // ARIA role for a menu separator. - SEPARATOR: 'separator', - - // ARIA role for a slider. - SLIDER: 'slider', - - // ARIA role for a spin button. - SPINBUTTON: 'spinbutton', - - // ARIA role for a live region with advisory info less severe than an alert. - STATUS: 'status', - - // ARIA role for a tab button. - TAB: 'tab', - - // ARIA role for a table. - TABLE: 'table', - - // ARIA role for a tab bar (i.e. a list of tab buttons). - TABLIST: 'tablist', - - // ARIA role for a tab page (i.e. the element holding tab contents). - TABPANEL: 'tabpanel', - - // ARIA role for a textbox element. - TEXTBOX: 'textbox', - - // ARIA role for a textinfo element. - TEXTINFO: 'textinfo', - - // ARIA role for an element displaying elapsed time or time remaining. - TIMER: 'timer', - - // ARIA role for a toolbar element. - TOOLBAR: 'toolbar', - - // ARIA role for a tooltip element. - TOOLTIP: 'tooltip', - // ARIA role for a tree. TREE: 'tree', - // ARIA role for a grid whose rows can be expanded and collapsed like a tree. - TREEGRID: 'treegrid', - // ARIA role for a tree item that sometimes may be expanded or collapsed. TREEITEM: 'treeitem' }; @@ -246,65 +92,15 @@ Blockly.utils.aria.State = { // for example the selected item in a list box. Value: ID of an element. ACTIVEDESCENDANT: 'activedescendant', - // ARIA property that, if true, indicates that all of a changed region should - // be presented, instead of only parts. Value: one of {true, false}. - ATOMIC: 'atomic', - - // ARIA property to specify that input completion is provided. Value: - // one of {'inline', 'list', 'both', 'none'}. - AUTOCOMPLETE: 'autocomplete', - - // ARIA state to indicate that an element and its subtree are being updated. - // Value: one of {true, false}. - BUSY: 'busy', - - // ARIA state for a checked item. Value: one of {'true', 'false', 'mixed', - // undefined}. - CHECKED: 'checked', - - // ARIA state that defines an element's column index or position with respect - // to the total number of columns within a table, grid, or treegrid. - // Value: number. - COLINDEX: 'colindex', - - // ARIA property that identifies the element or elements whose contents or - // presence are controlled by this element. - // Value: space-separated IDs of other elements. - CONTROLS: 'controls', - - // ARIA property that identifies the element or elements that describe - // this element. Value: space-separated IDs of other elements. - DESCRIBEDBY: 'describedby', - - // ARIA state for a disabled item. Value: one of {true, false}. - DISABLED: 'disabled', - - // ARIA property that indicates what functions can be performed when a - // dragged object is released on the drop target. Value: one of - // {'copy', 'move', 'link', 'execute', 'popup', 'none'}. - DROPEFFECT: 'dropeffect', + // ARIA property defines the total number of columns in a table, grid, or + // treegrid. + // Value: integer. + COLCOUNT: 'colcount', // ARIA state for setting whether the element like a tree node is expanded. // Value: one of {true, false, undefined}. EXPANDED: 'expanded', - // ARIA property that identifies the next element (or elements) in the - // recommended reading order of content. Value: space-separated ids of - // elements to flow to. - FLOWTO: 'flowto', - - // ARIA state that indicates an element's "grabbed" state in drag-and-drop. - // Value: one of {true, false, undefined}. - GRABBED: 'grabbed', - - // ARIA property indicating whether the element has a popup. - // Value: one of {true, false}. - HASPOPUP: 'haspopup', - - // ARIA state indicating that the element is not visible or perceivable - // to any user. Value: one of {true, false}. - HIDDEN: 'hidden', - // ARIA state indicating that the entered value does not conform. Value: // one of {false, true, 'grammar', 'spelling'} INVALID: 'invalid', @@ -321,54 +117,18 @@ Blockly.utils.aria.State = { // Value: integer. LEVEL: 'level', - // ARIA property indicating that an element will be updated, and - // describes the types of updates the user agents, assistive technologies, - // and user can expect from the live region. Value: one of {'off', 'polite', - // 'assertive'}. - LIVE: 'live', - - // ARIA property indicating whether a text box can accept multiline input. - // Value: one of {true, false}. - MULTILINE: 'multiline', - - // ARIA property indicating if the user may select more than one item. - // Value: one of {true, false}. - MULTISELECTABLE: 'multiselectable', - // ARIA property indicating if the element is horizontal or vertical. // Value: one of {'vertical', 'horizontal'}. ORIENTATION: 'orientation', - // ARIA property creating a visual, functional, or contextual parent/child - // relationship when the DOM hierarchy can't be used to represent it. - // Value: Space-separated IDs of elements. - OWNS: 'owns', - // ARIA property that defines an element's number of position in a list. // Value: integer. POSINSET: 'posinset', - // ARIA state for a pressed item. - // Value: one of {true, false, undefined, 'mixed'}. - PRESSED: 'pressed', - - // ARIA property indicating that an element is not editable. - // Value: one of {true, false}. - READONLY: 'readonly', - - // ARIA property indicating that change notifications within this subtree - // of a live region should be announced. Value: one of {'additions', - // 'removals', 'text', 'all', 'additions text'}. - RELEVANT: 'relevant', - - // ARIA property indicating that user input is required on this element - // before a form may be submitted. Value: one of {true, false}. - REQUIRED: 'required', - - // ARIA state that defines an element's row index or position with respect - // to the total number of rows within a table, grid, or treegrid. - // Value: number. - ROWINDEX: 'rowindex', + // ARIA property defines the total number of rows in a table, grid, or + // treegrid. + // Value: integer. + ROWCOUNT: 'rowcount', // ARIA state for setting the currently selected item in the list. // Value: one of {true, false, undefined}. @@ -377,72 +137,30 @@ Blockly.utils.aria.State = { // ARIA property defining the number of items in a list. Value: integer. SETSIZE: 'setsize', - // ARIA property indicating if items are sorted. Value: one of {'ascending', - // 'descending', 'none', 'other'}. - SORT: 'sort', - // ARIA property for slider maximum value. Value: number. VALUEMAX: 'valuemax', // ARIA property for slider minimum value. Value: number. - VALUEMIN: 'valuemin', - - // ARIA property for slider active value. Value: number. - VALUENOW: 'valuenow', - - // ARIA property for slider active value represented as text. - // Value: string. - VALUETEXT: 'valuetext' + VALUEMIN: 'valuemin' }; /** - * Sets the role of an element. If the roleName is - * empty string or null, the role for the element is removed. - * We encourage clients to call the goog.a11y.aria.removeRole - * method instead of setting null and empty string values. - * Special handling for this case is added to ensure - * backword compatibility with existing code. + * Sets the role of an element. * * Similar to Closure's goog.a11y.aria * * @param {!Element} element DOM node to set role of. - * @param {!Blockly.utils.aria.Role|string} roleName role name(s). + * @param {!Blockly.utils.aria.Role} roleName Role name. */ Blockly.utils.aria.setRole = function(element, roleName) { - if (!roleName) { - // Setting the ARIA role to empty string is not allowed - // by the ARIA standard. - Blockly.utils.aria.removeRole(element); - } else { - element.setAttribute(Blockly.utils.aria.ROLE_ATTRIBUTE_, roleName); - } -}; - -/** - * Gets role of an element. - * Copied from Closure's goog.a11y.aria - * @param {!Element} element DOM element to get role of. - * @return {?Blockly.utils.aria.Role} ARIA Role name. - */ -Blockly.utils.aria.getRole = function(element) { - var role = element.getAttribute(Blockly.utils.aria.ROLE_ATTRIBUTE_); - return /** @type {Blockly.utils.aria.Role} */ (role) || null; -}; - -/** - * Removes role of an element. - * Copied from Closure's goog.a11y.aria - * @param {!Element} element DOM element to remove the role from. - */ -Blockly.utils.aria.removeRole = function(element) { - element.removeAttribute(Blockly.utils.aria.ROLE_ATTRIBUTE_); + element.setAttribute(Blockly.utils.aria.ROLE_ATTRIBUTE_, roleName); }; /** * Sets the state or property of an element. * Copied from Closure's goog.a11y.aria * @param {!Element} element DOM node where we set state. - * @param {!(Blockly.utils.aria.State|string)} stateName State attribute being set. + * @param {!Blockly.utils.aria.State} stateName State attribute being set. * Automatically adds prefix 'aria-' to the state name if the attribute is * not an extra attribute. * @param {string|boolean|number|!Array.} value Value @@ -452,18 +170,6 @@ Blockly.utils.aria.setState = function(element, stateName, value) { if (Array.isArray(value)) { value = value.join(' '); } - var attrStateName = Blockly.utils.aria.getAriaAttributeName_(stateName); + var attrStateName = Blockly.utils.aria.ARIA_PREFIX_ + stateName; element.setAttribute(attrStateName, value); }; - -/** - * Adds the 'aria-' prefix to ariaName. - * Copied from Closure's goog.a11y.aria - * @param {string} ariaName ARIA state/property name. - * @private - * @return {string} The ARIA attribute name with added 'aria-' prefix. - * @throws {Error} If no such attribute exists. - */ -Blockly.utils.aria.getAriaAttributeName_ = function(ariaName) { - return Blockly.utils.aria.ARIA_PREFIX_ + ariaName; -}; diff --git a/core/utils/colour.js b/core/utils/colour.js index be304ef3e..fb1c82709 100644 --- a/core/utils/colour.js +++ b/core/utils/colour.js @@ -29,17 +29,16 @@ */ goog.provide('Blockly.utils.colour'); -goog.require('Blockly.utils'); - /** * Parses a colour from a string. * .parse('red') -> '#ff0000' * .parse('#f00') -> '#ff0000' * .parse('#ff0000') -> '#ff0000' + * .parse('0xff0000') -> '#ff0000' * .parse('rgb(255, 0, 0)') -> '#ff0000' - * @param {string} str Colour in some CSS format. - * @return {string|null} A string containing a hex representation of the colour, + * @param {string|number} str Colour in some CSS format. + * @return {?string} A string containing a hex representation of the colour, * or null if can't be parsed. */ Blockly.utils.colour.parse = function(str) { @@ -49,7 +48,8 @@ Blockly.utils.colour.parse = function(str) { // e.g. 'red' return hex; } - hex = str[0] == '#' ? str : '#' + str; + hex = str.substring(0, 2) == '0x' ? '#' + str.substring(2) : str; + hex = hex[0] == '#' ? hex : '#' + hex; if (/^#[0-9a-f]{6}$/.test(hex)) { // e.g. '#00ff88' return hex; @@ -87,12 +87,18 @@ Blockly.utils.colour.rgbToHex = function(r, g, b) { }; /** - * Converts a hex representation of a colour to RGB. - * @param {string} hexColor Colour in '#ff0000' format. + * Converts a colour to RGB. + * @param {string} colour String representing colour in any + * colour format ('#ff0000', 'red', '0xff000', etc). * @return {!Array.} RGB representation of the colour. */ -Blockly.utils.colour.hexToRgb = function(hexColor) { - var rgb = parseInt(hexColor.substr(1), 16); +Blockly.utils.colour.hexToRgb = function(colour) { + var hex = Blockly.utils.colour.parse(colour); + if (!hex) { + return [0, 0, 0]; + } + + var rgb = parseInt(hex.substr(1), 16); var r = rgb >> 16; var g = (rgb >> 8) & 255; var b = rgb & 255; @@ -166,11 +172,19 @@ Blockly.utils.colour.hsvToHex = function(h, s, v) { * @param {string} colour2 Second colour. * @param {number} factor The weight to be given to colour1 over colour2. * Values should be in the range [0, 1]. - * @return {string} Combined colour represented in hex. + * @return {?string} Combined colour represented in hex. */ Blockly.utils.colour.blend = function(colour1, colour2, factor) { - var rgb1 = Blockly.utils.colour.hexToRgb(Blockly.utils.colour.parse(colour1)); - var rgb2 = Blockly.utils.colour.hexToRgb(Blockly.utils.colour.parse(colour2)); + var hex1 = Blockly.utils.colour.parse(colour1); + if (!hex1) { + return null; + } + var hex2 = Blockly.utils.colour.parse(colour2); + if (!hex2) { + return null; + } + var rgb1 = Blockly.utils.colour.hexToRgb(hex1); + var rgb2 = Blockly.utils.colour.hexToRgb(hex2); var r = Math.round(rgb2[0] + factor * (rgb1[0] - rgb2[0])); var g = Math.round(rgb2[1] + factor * (rgb1[1] - rgb2[1])); var b = Math.round(rgb2[2] + factor * (rgb1[2] - rgb2[2])); diff --git a/core/utils/dom.js b/core/utils/dom.js index 045cc17ad..c2337e595 100644 --- a/core/utils/dom.js +++ b/core/utils/dom.js @@ -76,6 +76,13 @@ Blockly.utils.dom.cacheWidths_ = null; */ Blockly.utils.dom.cacheReference_ = 0; +/** + * A HTML canvas context used for computing text width. + * @type {CanvasRenderingContext2D} + * @private + */ +Blockly.utils.dom.canvasContext_ = null; + /** * Helper method for creating SVG elements. * @param {string} name Element's tag name. @@ -271,3 +278,52 @@ Blockly.utils.dom.getTextWidth = function(textElement) { } return width; }; + +/** + * Gets the width of a text element using a faster method than `getTextWidth`. + * This method requires that we know the text element's font family and size in + * advance. Similar to `getTextWidth`, we cache the width we compute. + * @param {!Element} textElement An SVG 'text' element. + * @param {number} fontSize The font size to use. + * @param {string} fontWeight The font weight to use. + * @param {string} fontFamily The font family to use. + * @return {number} Width of element. + */ +Blockly.utils.dom.getFastTextWidth = function(textElement, + fontSize, fontWeight, fontFamily) { + var text = textElement.textContent; + var key = text + '\n' + textElement.className.baseVal; + var width; + + // Return the cached width if it exists. + if (Blockly.utils.dom.cacheWidths_) { + width = Blockly.utils.dom.cacheWidths_[key]; + if (width) { + return width; + } + } + + if (!Blockly.utils.dom.canvasContext_) { + // Inject the canvas element used for computing text widths. + var computeCanvas = document.createElement('canvas'); + computeCanvas.className = 'blocklyComputeCanvas'; + document.body.appendChild(computeCanvas); + + // Initialize the HTML canvas context and set the font. + // The context font must match blocklyText's fontsize and font-family + // set in CSS. + Blockly.utils.dom.canvasContext_ = computeCanvas.getContext('2d'); + } + // Set the desired font size and family. + Blockly.utils.dom.canvasContext_.font = + fontWeight + ' ' + fontSize + 'pt ' + fontFamily; + + // Measure the text width using the helper canvas context. + width = Blockly.utils.dom.canvasContext_.measureText(text).width; + + // Cache the computed width and return. + if (Blockly.utils.dom.cacheWidths_) { + Blockly.utils.dom.cacheWidths_[key] = width; + } + return width; +}; diff --git a/core/utils/object.js b/core/utils/object.js index f8b6a55ba..2a9b848a9 100644 --- a/core/utils/object.js +++ b/core/utils/object.js @@ -55,7 +55,9 @@ Blockly.utils.object.mixin = function(target, source) { */ Blockly.utils.object.values = function(obj) { if (Object.values) { + /* eslint-disable es5/no-es6-methods */ return Object.values(obj); + /* eslint-enable es5/no-es6-methods */ } // Fallback for IE. return Object.keys(obj).map(function(e) { diff --git a/core/utils/style.js b/core/utils/style.js index 8f9ecddbb..3e01fe053 100644 --- a/core/utils/style.js +++ b/core/utils/style.js @@ -67,7 +67,7 @@ Blockly.utils.style.getSize = function(element) { /** * Gets the height and width of an element when the display is not none. * @param {!Element} element Element to get size of. - * @return {!goog.math.Size} Object with width/height properties. + * @return {!Blockly.utils.Size} Object with width/height properties. * @private */ Blockly.utils.style.getSizeWithDisplay_ = function(element) { diff --git a/core/utils/useragent.js b/core/utils/useragent.js index 0f72c7e1d..f34d73e74 100644 --- a/core/utils/useragent.js +++ b/core/utils/useragent.js @@ -51,6 +51,8 @@ goog.require('Blockly.utils.global'); // Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.44 // (KHTML, like Gecko) JavaFX/8.0 Safari/537.44 Blockly.utils.userAgent.JAVA_FX = has('JavaFX'); + Blockly.utils.userAgent.CHROME = (has('Chrome') || has('CriOS')) && + !Blockly.utils.userAgent.EDGE; // Engines. Logic from: // https://github.com/google/closure-library/blob/master/closure/goog/labs/useragent/engine.js diff --git a/core/utils/xml.js b/core/utils/xml.js index 054aac94f..ec6302b10 100644 --- a/core/utils/xml.js +++ b/core/utils/xml.js @@ -58,7 +58,7 @@ Blockly.utils.xml.createElement = function(tagName) { /** * Create text element for XML. * @param {string} text Text content. - * @return {!Node} New DOM node. + * @return {!Text} New DOM text node. * @public */ Blockly.utils.xml.createTextNode = function(text) { @@ -80,7 +80,7 @@ Blockly.utils.xml.textToDomDocument = function(text) { /** * Converts a DOM structure into plain text. * Currently the text format is fairly ugly: all one line with no whitespace. - * @param {!Element} dom A tree of XML elements. + * @param {!Node} dom A tree of XML nodes. * @return {string} Text representation. * @public */ diff --git a/core/variable_map.js b/core/variable_map.js index 3e13d67d2..dc8bf709e 100644 --- a/core/variable_map.js +++ b/core/variable_map.js @@ -28,6 +28,7 @@ goog.require('Blockly.Events.VarDelete'); goog.require('Blockly.Events.VarRename'); goog.require('Blockly.Msg'); goog.require('Blockly.utils'); +goog.require('Blockly.utils.object'); /** @@ -168,7 +169,7 @@ Blockly.VariableMap.prototype.renameVariableWithConflict_ = function(variable, * their type. This will default to '' which is a specific type. * @param {?string=} opt_id The unique ID of the variable. This will default to * a UUID. - * @return {Blockly.VariableModel} The newly created variable. + * @return {!Blockly.VariableModel} The newly created variable. */ Blockly.VariableMap.prototype.createVariable = function(name, opt_type, opt_id) { @@ -208,7 +209,7 @@ Blockly.VariableMap.prototype.createVariable = function(name, */ Blockly.VariableMap.prototype.deleteVariable = function(variable) { var variableList = this.variableMap_[variable.type]; - for (var i = 0, tempVar; tempVar = variableList[i]; i++) { + for (var i = 0, tempVar; (tempVar = variableList[i]); i++) { if (tempVar.getId() == variable.getId()) { variableList.splice(i, 1); Blockly.Events.fire(new Blockly.Events.VarDelete(variable)); @@ -228,7 +229,7 @@ Blockly.VariableMap.prototype.deleteVariableById = function(id) { // Check whether this variable is a function parameter before deleting. var variableName = variable.name; var uses = this.getVariableUsesById(id); - for (var i = 0, block; block = uses[i]; i++) { + for (var i = 0, block; (block = uses[i]); i++) { if (block.type == 'procedures_defnoreturn' || block.type == 'procedures_defreturn') { var procedureName = block.getFieldValue('NAME'); @@ -248,13 +249,13 @@ Blockly.VariableMap.prototype.deleteVariableById = function(id) { replace('%2', variableName); Blockly.confirm(confirmText, function(ok) { - if (ok) { - map.deleteVariableInternal_(variable, uses); + if (ok && variable) { + map.deleteVariableInternal(variable, uses); } }); } else { // No confirmation necessary for a single block. - map.deleteVariableInternal_(variable, uses); + map.deleteVariableInternal(variable, uses); } } else { console.warn("Can't delete non-existent variable: " + id); @@ -266,9 +267,9 @@ Blockly.VariableMap.prototype.deleteVariableById = function(id) { * user for confirmation. * @param {!Blockly.VariableModel} variable Variable to delete. * @param {!Array.} uses An array of uses of the variable. - * @private + * @package */ -Blockly.VariableMap.prototype.deleteVariableInternal_ = function(variable, +Blockly.VariableMap.prototype.deleteVariableInternal = function(variable, uses) { var existingGroup = Blockly.Events.getGroup(); if (!existingGroup) { @@ -276,7 +277,7 @@ Blockly.VariableMap.prototype.deleteVariableInternal_ = function(variable, } try { for (var i = 0; i < uses.length; i++) { - uses[i].dispose(true, false); + uses[i].dispose(true); } this.deleteVariable(variable); } finally { @@ -292,7 +293,7 @@ Blockly.VariableMap.prototype.deleteVariableInternal_ = function(variable, * Find the variable by the given name and type and return it. Return null if * it is not found. * @param {string} name The name to check for. - * @param {string=} opt_type The type of the variable. If not provided it + * @param {?string=} opt_type The type of the variable. If not provided it * defaults to the empty string, which is a specific type. * @return {Blockly.VariableModel} The variable with the given name, or null if * it was not found. @@ -301,7 +302,7 @@ Blockly.VariableMap.prototype.getVariable = function(name, opt_type) { var type = opt_type || ''; var list = this.variableMap_[type]; if (list) { - for (var j = 0, variable; variable = list[j]; j++) { + for (var j = 0, variable; (variable = list[j]); j++) { if (Blockly.Names.equals(variable.name, name)) { return variable; } @@ -320,7 +321,7 @@ Blockly.VariableMap.prototype.getVariableById = function(id) { var keys = Object.keys(this.variableMap_); for (var i = 0; i < keys.length; i++ ) { var key = keys[i]; - for (var j = 0, variable; variable = this.variableMap_[key][j]; j++) { + for (var j = 0, variable; (variable = this.variableMap_[key][j]); j++) { if (variable.getId() == id) { return variable; } @@ -355,11 +356,13 @@ Blockly.VariableMap.prototype.getVariablesOfType = function(type) { * @package */ Blockly.VariableMap.prototype.getVariableTypes = function(ws) { - var potentialTypes = []; + var variableMap = {}; + Blockly.utils.object.mixin(variableMap, this.variableMap_); if (ws && ws.getPotentialVariableMap()) { - potentialTypes = Object.keys(ws.getPotentialVariableMap().variableMap_); + Blockly.utils.object.mixin(variableMap, + ws.getPotentialVariableMap().variableMap_); } - var types = Object.keys(this.variableMap_).concat(potentialTypes); + var types = Object.keys(variableMap); var hasEmpty = false; for (var i = 0; i < types.length; i++) { if (types[i] == '') { @@ -384,6 +387,21 @@ Blockly.VariableMap.prototype.getAllVariables = function() { return all_variables; }; +/** + * Returns 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 = []; + for (var key in this.variableMap_) { + var variables = this.variableMap_[key]; + for (var i = 0, variable; (variable = variables[i]); i++) { + allNames.push(variable.name); + } + } + return allNames; +}; + /** * Find all the uses of a named variable. * @param {string} id ID of the variable to find. diff --git a/core/variables.js b/core/variables.js index 74ca01bf4..5e260990d 100644 --- a/core/variables.js +++ b/core/variables.js @@ -107,7 +107,7 @@ Blockly.Variables.ALL_DEVELOPER_VARS_WARNINGS_BY_BLOCK_TYPE_ = {}; Blockly.Variables.allDeveloperVariables = function(workspace) { var blocks = workspace.getAllBlocks(false); var variableHash = Object.create(null); - for (var i = 0, block; block = blocks[i]; i++) { + for (var i = 0, block; (block = blocks[i]); i++) { var getDeveloperVariables = block.getDeveloperVariables; if (!getDeveloperVariables && block.getDeveloperVars) { // August 2018: getDeveloperVars() was deprecated and renamed @@ -194,7 +194,7 @@ Blockly.Variables.flyoutCategoryBlocks = function(workspace) { if (Blockly.Blocks['variables_get']) { variableModelList.sort(Blockly.VariableModel.compareByName); - for (var i = 0, variable; variable = variableModelList[i]; i++) { + for (var i = 0, variable; (variable = variableModelList[i]); i++) { var block = Blockly.utils.xml.createElement('block'); block.setAttribute('type', 'variables_get'); block.setAttribute('gap', 8); @@ -206,6 +206,8 @@ Blockly.Variables.flyoutCategoryBlocks = function(workspace) { return xmlList; }; +Blockly.Variables.VAR_LETTER_OPTIONS = 'ijkmnopqrstuvwxyzabcdefgh'; // No 'l'. + /** * Return a new variable name that is not yet being used. This will try to * generate single letter variable names in the range 'i' to 'z' to start with. @@ -215,44 +217,51 @@ Blockly.Variables.flyoutCategoryBlocks = function(workspace) { * @return {string} New variable name. */ Blockly.Variables.generateUniqueName = function(workspace) { - var variableList = workspace.getAllVariables(); - var newName = ''; - if (variableList.length) { - var nameSuffix = 1; - var letters = 'ijkmnopqrstuvwxyzabcdefgh'; // No 'l'. - var letterIndex = 0; - var potName = letters.charAt(letterIndex); - while (!newName) { - var inUse = false; - for (var i = 0; i < variableList.length; i++) { - if (variableList[i].name.toLowerCase() == potName) { - // This potential name is already used. - inUse = true; - break; - } - } - if (inUse) { - // Try the next potential name. - letterIndex++; - if (letterIndex == letters.length) { - // Reached the end of the character sequence so back to 'i'. - // a new suffix. - letterIndex = 0; - nameSuffix++; - } - potName = letters.charAt(letterIndex); - if (nameSuffix > 1) { - potName += nameSuffix; - } - } else { - // We can use the current potential name. - newName = potName; + return Blockly.Variables.generateUniqueNameFromOptions( + Blockly.Variables.VAR_LETTER_OPTIONS.charAt(0), + workspace.getAllVariableNames() + ); +}; + +/** + * Returns a unique name that is not present in the usedNames array. This + * 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. + * @return {string} A unique name that is not present in the usedNames array. + */ +Blockly.Variables.generateUniqueNameFromOptions = function(startChar, usedNames) { + if (!usedNames.length) { + return startChar; + } + + var letters = Blockly.Variables.VAR_LETTER_OPTIONS; + var suffix = ''; + var letterIndex = letters.indexOf(startChar); + var potName = startChar; + + // eslint-disable-next-line no-constant-condition + while (true) { + var inUse = false; + for (var i = 0; i < usedNames.length; i++) { + if (usedNames[i].toLowerCase() == potName) { + inUse = true; + break; } } - } else { - newName = 'i'; + if (!inUse) { + return potName; + } + + letterIndex++; + if (letterIndex == letters.length) { + // Reached the end of the character sequence so back to 'i'. + letterIndex = 0; + suffix = Number(suffix) + 1; + } + potName = letters.charAt(letterIndex) + suffix; } - return newName; }; /** @@ -283,14 +292,13 @@ Blockly.Variables.createVariableButtonHandler = function( var existing = Blockly.Variables.nameUsedWithAnyType_(text, workspace); if (existing) { - var lowerCase = text.toLowerCase(); if (existing.type == type) { var msg = Blockly.Msg['VARIABLE_ALREADY_EXISTS'].replace( - '%1', lowerCase); + '%1', existing.name); } else { var msg = Blockly.Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE']; - msg = msg.replace('%1', lowerCase).replace('%2', existing.type); + msg = msg.replace('%1', existing.name).replace('%2', existing.type); } Blockly.alert(msg, function() { @@ -351,7 +359,7 @@ Blockly.Variables.renameVariable = function(workspace, variable, variable.type, workspace); if (existing) { var msg = Blockly.Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE'] - .replace('%1', newName.toLowerCase()) + .replace('%1', existing.name) .replace('%2', existing.type); Blockly.alert(msg, function() { @@ -412,7 +420,7 @@ Blockly.Variables.nameUsedWithOtherType_ = function(name, type, workspace) { var allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); - for (var i = 0, variable; variable = allVariables[i]; i++) { + for (var i = 0, variable; (variable = allVariables[i]); i++) { if (variable.name.toLowerCase() == name && variable.type != type) { return variable; } @@ -433,7 +441,7 @@ Blockly.Variables.nameUsedWithAnyType_ = function(name, workspace) { var allVariables = workspace.getVariableMap().getAllVariables(); name = name.toLowerCase(); - for (var i = 0, variable; variable = allVariables[i]; i++) { + for (var i = 0, variable; (variable = allVariables[i]); i++) { if (variable.name.toLowerCase() == name) { return variable; } @@ -501,10 +509,11 @@ Blockly.Variables.getOrCreateVariablePackage = function(workspace, id, opt_name, */ Blockly.Variables.getVariable = function(workspace, id, opt_name, opt_type) { var potentialVariableMap = workspace.getPotentialVariableMap(); + var variable = null; // Try to just get the variable, by ID if possible. if (id) { // Look in the real variable map before checking the potential variable map. - var variable = workspace.getVariableById(id); + variable = workspace.getVariableById(id); if (!variable && potentialVariableMap) { variable = potentialVariableMap.getVariableById(id); } @@ -519,7 +528,7 @@ Blockly.Variables.getVariable = function(workspace, id, opt_name, opt_type) { throw Error('Tried to look up a variable by name without a type'); } // Otherwise look up by name and type. - var variable = workspace.getVariable(opt_name, opt_type); + variable = workspace.getVariable(opt_name, opt_type); if (!variable && potentialVariableMap) { variable = potentialVariableMap.getVariable(opt_name, opt_type); } @@ -548,10 +557,11 @@ Blockly.Variables.createVariable_ = function(workspace, id, opt_name, } // Create a potential variable if in the flyout. + var variable = null; if (potentialVariableMap) { - var variable = potentialVariableMap.createVariable(opt_name, opt_type, id); + variable = potentialVariableMap.createVariable(opt_name, opt_type, id); } else { // In the main workspace, create a real variable. - var variable = workspace.createVariable(opt_name, opt_type, id); + variable = workspace.createVariable(opt_name, opt_type, id); } return variable; }; diff --git a/core/variables_dynamic.js b/core/variables_dynamic.js index e07f03eae..e7490dd19 100644 --- a/core/variables_dynamic.js +++ b/core/variables_dynamic.js @@ -33,15 +33,15 @@ goog.require('Blockly.VariableModel'); Blockly.VariablesDynamic.onCreateVariableButtonClick_String = function(button) { Blockly.Variables.createVariableButtonHandler(button.getTargetWorkspace(), - null, 'String'); + undefined, 'String'); }; Blockly.VariablesDynamic.onCreateVariableButtonClick_Number = function(button) { Blockly.Variables.createVariableButtonHandler(button.getTargetWorkspace(), - null, 'Number'); + undefined, 'Number'); }; Blockly.VariablesDynamic.onCreateVariableButtonClick_Colour = function(button) { Blockly.Variables.createVariableButtonHandler(button.getTargetWorkspace(), - null, 'Colour'); + undefined, 'Colour'); }; /** * Construct the elements (blocks and button) required by the flyout for the @@ -98,7 +98,7 @@ Blockly.VariablesDynamic.flyoutCategoryBlocks = function(workspace) { } if (Blockly.Blocks['variables_get_dynamic']) { variableModelList.sort(Blockly.VariableModel.compareByName); - for (var i = 0, variable; variable = variableModelList[i]; i++) { + for (var i = 0, variable; (variable = variableModelList[i]); i++) { var block = Blockly.utils.xml.createElement('block'); block.setAttribute('type', 'variables_get_dynamic'); block.setAttribute('gap', 8); diff --git a/core/warning.js b/core/warning.js index e6d3dcc71..6d0f4d9eb 100644 --- a/core/warning.js +++ b/core/warning.js @@ -92,7 +92,7 @@ Blockly.Warning.textToDom_ = function(text) { (Blockly.utils.dom.createSvgElement( 'text', { - 'class': 'blocklyText blocklyBubbleText', + 'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents', 'y': Blockly.Bubble.BORDER_WIDTH }, null) @@ -134,7 +134,8 @@ Blockly.Warning.prototype.createBubble = function() { this.paragraphElement_ = Blockly.Warning.textToDom_(this.getText()); this.bubble_ = new Blockly.Bubble( /** @type {!Blockly.WorkspaceSvg} */ (this.block_.workspace), - this.paragraphElement_, this.block_.svgPath_, this.iconXY_, null, null); + this.paragraphElement_, this.block_.pathObject.svgPath, + /** @type {!Blockly.utils.Coordinate} */ (this.iconXY_), null, null); // Expose this warning's block's ID on its top-level SVG group. this.bubble_.setSvgId(this.block_.id); if (this.block_.RTL) { @@ -142,13 +143,13 @@ Blockly.Warning.prototype.createBubble = function() { // This cannot be done until the bubble is rendered on screen. var maxWidth = this.paragraphElement_.getBBox().width; for (var i = 0, textElement; - textElement = this.paragraphElement_.childNodes[i]; i++) { + (textElement = this.paragraphElement_.childNodes[i]); i++) { textElement.setAttribute('text-anchor', 'end'); textElement.setAttribute('x', maxWidth + Blockly.Bubble.BORDER_WIDTH); } } - this.updateColour(); + this.applyColour(); }; /** @@ -171,7 +172,7 @@ Blockly.Warning.prototype.disposeBubble = function() { */ Blockly.Warning.prototype.bodyFocus_ = function(_e) { - this.bubble_.promote_(); + this.bubble_.promote(); }; /** diff --git a/core/widgetdiv.js b/core/widgetdiv.js index 0c846ed1f..326ee0cff 100644 --- a/core/widgetdiv.js +++ b/core/widgetdiv.js @@ -32,12 +32,6 @@ goog.provide('Blockly.WidgetDiv'); goog.require('Blockly.utils.style'); -/** - * The HTML container. Set once by Blockly.WidgetDiv.createDom. - * @type {Element} - */ -Blockly.WidgetDiv.DIV = null; - /** * The object currently using this container. * @type {Object} @@ -52,6 +46,20 @@ Blockly.WidgetDiv.owner_ = null; */ Blockly.WidgetDiv.dispose_ = null; +/** + * A class name representing the current owner's workspace renderer. + * @type {?string} + * @private + */ +Blockly.WidgetDiv.rendererClassName_ = null; + +/** + * A class name representing the current owner's workspace theme. + * @type {?string} + * @private + */ +Blockly.WidgetDiv.themeClassName_ = null; + /** * Create the widget div and inject it onto the page. */ @@ -59,7 +67,10 @@ Blockly.WidgetDiv.createDom = function() { if (Blockly.WidgetDiv.DIV) { return; // Already created. } - // Create an HTML container for popup overlays (e.g. editor widgets). + /** + * The HTML container for popup overlays (e.g. editor widgets). + * @type {!Element} + */ Blockly.WidgetDiv.DIV = document.createElement('div'); Blockly.WidgetDiv.DIV.className = 'blocklyWidgetDiv'; document.body.appendChild(Blockly.WidgetDiv.DIV); @@ -76,27 +87,40 @@ Blockly.WidgetDiv.show = function(newOwner, rtl, dispose) { Blockly.WidgetDiv.hide(); Blockly.WidgetDiv.owner_ = newOwner; Blockly.WidgetDiv.dispose_ = dispose; - // Temporarily move the widget to the top of the screen so that it does not - // cause a scrollbar jump in Firefox when displayed. - var xy = Blockly.utils.style.getViewportPageOffset(); - Blockly.WidgetDiv.DIV.style.top = xy.y + 'px'; - Blockly.WidgetDiv.DIV.style.direction = rtl ? 'rtl' : 'ltr'; - Blockly.WidgetDiv.DIV.style.display = 'block'; + var div = Blockly.WidgetDiv.DIV; + div.style.direction = rtl ? 'rtl' : 'ltr'; + div.style.display = 'block'; + Blockly.WidgetDiv.rendererClassName_ = + Blockly.getMainWorkspace().getRenderer().name + '-renderer'; + Blockly.WidgetDiv.themeClassName_ = + Blockly.getMainWorkspace().getTheme().name + '-theme'; + Blockly.utils.dom.addClass(div, Blockly.WidgetDiv.rendererClassName_); + Blockly.utils.dom.addClass(div, Blockly.WidgetDiv.themeClassName_); }; /** * Destroy the widget and hide the div. */ Blockly.WidgetDiv.hide = function() { + var div = Blockly.WidgetDiv.DIV; if (Blockly.WidgetDiv.owner_) { Blockly.WidgetDiv.owner_ = null; - Blockly.WidgetDiv.DIV.style.display = 'none'; - Blockly.WidgetDiv.DIV.style.left = ''; - Blockly.WidgetDiv.DIV.style.top = ''; + div.style.display = 'none'; + div.style.left = ''; + div.style.top = ''; Blockly.WidgetDiv.dispose_ && Blockly.WidgetDiv.dispose_(); Blockly.WidgetDiv.dispose_ = null; - Blockly.WidgetDiv.DIV.innerHTML = ''; + div.innerHTML = ''; } + if (Blockly.WidgetDiv.rendererClassName_) { + Blockly.utils.dom.removeClass(div, Blockly.WidgetDiv.rendererClassName_); + Blockly.WidgetDiv.rendererClassName_ = null; + } + if (Blockly.WidgetDiv.themeClassName_) { + Blockly.utils.dom.removeClass(div, Blockly.WidgetDiv.themeClassName_); + Blockly.WidgetDiv.themeClassName_ = null; + } + Blockly.getMainWorkspace().markFocused(); }; /** diff --git a/core/workspace.js b/core/workspace.js index 1d615dbae..0596c2d8b 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -23,11 +23,8 @@ goog.provide('Blockly.Workspace'); -goog.require('Blockly.Cursor'); -goog.require('Blockly.MarkerCursor'); goog.require('Blockly.Events'); -goog.require('Blockly.ThemeManager'); -goog.require('Blockly.Themes.Classic'); +goog.require('Blockly.Options'); goog.require('Blockly.utils'); goog.require('Blockly.utils.math'); goog.require('Blockly.VariableMap'); @@ -44,7 +41,8 @@ Blockly.Workspace = function(opt_options) { this.id = Blockly.utils.genUid(); Blockly.Workspace.WorkspaceDB_[this.id] = this; /** @type {!Blockly.Options} */ - this.options = opt_options || {}; + this.options = opt_options || + new Blockly.Options(/** @type {!Blockly.BlocklyOptions} */ ({})); /** @type {boolean} */ this.RTL = !!this.options.RTL; /** @type {boolean} */ @@ -109,35 +107,10 @@ Blockly.Workspace = function(opt_options) { * A FieldVariable must always refer to a Blockly.VariableModel. We reconcile * these by tracking "potential" variables in the flyout. These variables * become real when references to them are dragged into the main workspace. - * @type {!Blockly.VariableMap} + * @type {Blockly.VariableMap} * @private */ this.potentialVariableMap_ = null; - - /** - * The cursor used to navigate around the AST for keyboard navigation. - * @type {!Blockly.Cursor} - * @protected - */ - this.cursor_ = new Blockly.Cursor(); - - /** - * The marker used to mark a location for keyboard navigation. - * @type {!Blockly.MarkerCursor} - * @protected - */ - this.marker_ = new Blockly.MarkerCursor(); - - /** - * Object in charge of storing and updating the workspace theme. - * @type {!Blockly.ThemeManager} - * @protected - */ - this.themeManager_ = this.options.parentWorkspace ? - this.options.parentWorkspace.getThemeManager() : - new Blockly.ThemeManager(this.options.theme || Blockly.Themes.Classic); - - this.themeManager_.subscribeWorkspace(this); }; /** @@ -166,93 +139,6 @@ Blockly.Workspace.prototype.MAX_UNDO = 1024; */ Blockly.Workspace.prototype.connectionDBList = null; -/** - * Sets the cursor for keyboard navigation. - * @param {Blockly.Cursor} cursor The cursor used to navigate around the Blockly - * AST for keyboard navigation. - */ -Blockly.Workspace.prototype.setCursor = function(cursor) { - this.cursor_ = cursor; -}; - -/** - * Sets the marker for keyboard navigation. - * @param {Blockly.MarkerCursor} marker The marker used to mark a location for - * keyboard navigation. - */ -Blockly.Workspace.prototype.setMarker = function(marker) { - this.marker_ = marker; -}; - -/** - * Get the cursor used to navigate around the AST for keyboard navigation. - * @return {Blockly.Cursor} The cursor for this workspace. - */ -Blockly.Workspace.prototype.getCursor = function() { - return this.cursor_; -}; - -/** - * Get the marker used to mark a location for keyboard navigation. - * @return {Blockly.MarkerCursor} the marker for this workspace. - */ -Blockly.Workspace.prototype.getMarker = function() { - return this.marker_; -}; - -/** - * Get the workspace theme object. - * @return {!Blockly.Theme} The workspace theme object. - */ -Blockly.Workspace.prototype.getTheme = function() { - return this.themeManager_.getTheme(); -}; - -/** - * Set the workspace theme object. - * If no theme is passed, default to the `Blockly.Themes.Classic` theme. - * @param {Blockly.Theme} theme The workspace theme object. - */ -Blockly.Workspace.prototype.setTheme = function(theme) { - if (!theme) { - theme = /** @type {!Blockly.Theme} */ (Blockly.Themes.Classic); - } - this.themeManager_.setTheme(theme); -}; - -/** - * Refresh all blocks on the workspace after a theme update. - * @package - */ -Blockly.Workspace.prototype.refreshTheme = function() { - // Update all blocks in workspace that have a style name. - this.updateBlockStyles_(this.getAllBlocks().filter( - function(block) { - return block.getStyleName() !== undefined; - } - )); - - var event = new Blockly.Events.Ui(null, 'theme', null, null); - event.workspaceId = this.id; - Blockly.Events.fire(event); -}; - -/** - * Updates all the blocks with new style. - * @param {!Array.} blocks List of blocks to update the style - * on. - * @private - */ -Blockly.Workspace.prototype.updateBlockStyles_ = function(blocks) { - for (var i = 0, block; block = blocks[i]; i++) { - var blockStyleName = block.getStyleName(); - block.setStyle(blockStyleName); - if (block.mutator) { - block.mutator.updateBlockStyle(blockStyleName); - } - } -}; - /** * Dispose of this workspace. * Unlink from all DOM elements to prevent memory leaks. @@ -263,15 +149,6 @@ Blockly.Workspace.prototype.dispose = function() { this.clear(); // Remove from workspace database. delete Blockly.Workspace.WorkspaceDB_[this.id]; - - if (this.themeManager_) { - this.themeManager_.unsubscribeWorkspace(this); - this.themeManager_.unsubscribe(this.svgBackground_); - if (!this.options.parentWorkspace) { - this.themeManager_.dispose(); - this.themeManager_ = null; - } - } }; /** @@ -479,10 +356,10 @@ Blockly.Workspace.prototype.clear = function() { Blockly.Events.setGroup(true); } while (this.topBlocks_.length) { - this.topBlocks_[0].dispose(); + this.topBlocks_[0].dispose(false); } while (this.topComments_.length) { - this.topComments_[this.topComments_.length - 1].dispose(); + this.topComments_[this.topComments_.length - 1].dispose(false); } if (!existingGroup) { Blockly.Events.setGroup(false); @@ -516,7 +393,7 @@ Blockly.Workspace.prototype.renameVariableById = function(id, newName) { * their type. This will default to '' which is a specific type. * @param {?string=} opt_id The unique ID of the variable. This will default to * a UUID. - * @return {Blockly.VariableModel} The newly created variable. + * @return {!Blockly.VariableModel} The newly created variable. */ Blockly.Workspace.prototype.createVariable = function(name, opt_type, opt_id) { return this.variableMap_.createVariable(name, opt_type, opt_id); @@ -548,7 +425,7 @@ Blockly.Workspace.prototype.deleteVariableById = function(id) { * @private */ Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable, uses) { - this.variableMap_.deleteVariableInternal_(variable, uses); + this.variableMap_.deleteVariableInternal(variable, uses); }; /** @@ -593,7 +470,7 @@ Blockly.Workspace.prototype.getVariableById = function(id) { * Find the variable with the specified type. If type is null, return list of * variables with empty string type. * @param {?string} type Type of the variables to find. - * @return {Array.} The sought after variables of the + * @return {!Array.} The sought after variables of the * passed in type. An empty array if none are found. */ Blockly.Workspace.prototype.getVariablesOfType = function(type) { @@ -617,6 +494,14 @@ Blockly.Workspace.prototype.getAllVariables = function() { return this.variableMap_.getAllVariables(); }; +/** + * Returns 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(); +}; + /* End functions that are just pass-throughs to the variable map. */ /** @@ -651,7 +536,7 @@ Blockly.Workspace.prototype.remainingCapacity = function() { return Infinity; } - return this.options.maxBlocks - this.getAllBlocks().length; + return this.options.maxBlocks - this.getAllBlocks(false).length; }; /** @@ -665,7 +550,7 @@ Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { return Infinity; } return (this.options.maxInstances[type] || Infinity) - - this.getBlocksByType(type).length; + this.getBlocksByType(type, false).length; }; /** @@ -722,13 +607,13 @@ Blockly.Workspace.prototype.undo = function(redo) { events.push(inputStack.pop()); } // Push these popped events on the opposite stack. - for (var i = 0, event; event = events[i]; i++) { + for (var i = 0, event; (event = events[i]); i++) { outputStack.push(event); } events = Blockly.Events.filter(events, redo); Blockly.Events.recordUndo = false; try { - for (var i = 0, event; event = events[i]; i++) { + for (var i = 0, event; (event = events[i]); i++) { event.run(redo); } } finally { @@ -779,7 +664,7 @@ Blockly.Workspace.prototype.fireChangeListener = function(event) { this.undoStack_.shift(); } } - for (var i = 0, func; func = this.listeners_[i]; i++) { + for (var i = 0, func; (func = this.listeners_[i]); i++) { func(event); } }; @@ -793,6 +678,25 @@ Blockly.Workspace.prototype.getBlockById = function(id) { return this.blockDB_[id] || null; }; +/** + * Set a block on this workspace with the specified ID. + * @param {string} id ID of block to set. + * @param {Blockly.Block} block The block to set. + * @package + */ +Blockly.Workspace.prototype.setBlockById = function(id, block) { + this.blockDB_[id] = block; +}; + +/** + * Delete a block off this workspace with the specified ID. + * @param {string} id ID of block to delete. + * @package + */ +Blockly.Workspace.prototype.removeBlockById = function(id) { + delete this.blockDB_[id]; +}; + /** * Find the comment on this workspace with the specified ID. * @param {string} id ID of comment to find. @@ -814,7 +718,7 @@ Blockly.Workspace.prototype.getCommentById = function(id) { Blockly.Workspace.prototype.allInputsFilled = function( opt_shadowBlocksAreFilled) { var blocks = this.getTopBlocks(false); - for (var i = 0, block; block = blocks[i]; i++) { + for (var i = 0, block; (block = blocks[i]); i++) { if (!block.allInputsFilled(opt_shadowBlocksAreFilled)) { return false; } @@ -842,12 +746,21 @@ Blockly.Workspace.prototype.createPotentialVariableMap = function() { /** * Return the map of all variables on the workspace. - * @return {Blockly.VariableMap} The variable map. + * @return {!Blockly.VariableMap} The variable map. */ Blockly.Workspace.prototype.getVariableMap = function() { return this.variableMap_; }; +/** + * Set the map of all variables on the workspace. + * @param {!Blockly.VariableMap} variableMap The variable map. + * @package + */ +Blockly.Workspace.prototype.setVariableMap = function(variableMap) { + this.variableMap_ = variableMap; +}; + /** * Database of all workspaces. * @private @@ -874,12 +787,3 @@ Blockly.Workspace.getAll = function() { } return workspaces; }; - -/** - * Get the theme manager for this workspace. - * @return {!Blockly.ThemeManager} The theme manager for this workspace. - * @package - */ -Blockly.Workspace.prototype.getThemeManager = function() { - return this.themeManager_; -}; diff --git a/core/workspace_audio.js b/core/workspace_audio.js index 256cd5166..1c24f742b 100644 --- a/core/workspace_audio.js +++ b/core/workspace_audio.js @@ -48,7 +48,6 @@ Blockly.WorkspaceAudio = function(parentWorkspace) { /** * Database of pre-loaded sounds. * @private - * @const */ this.SOUNDS_ = Object.create(null); }; diff --git a/core/workspace_comment.js b/core/workspace_comment.js index 66e494398..5d06eec21 100644 --- a/core/workspace_comment.js +++ b/core/workspace_comment.js @@ -95,6 +95,12 @@ Blockly.WorkspaceComment = function(workspace, content, height, width, opt_id) { */ this.movable_ = true; + /** + * @type {boolean} + * @private + */ + this.editable_ = true; + /** * @protected * @type {string} @@ -229,6 +235,23 @@ Blockly.WorkspaceComment.prototype.setMovable = function(movable) { this.movable_ = movable; }; +/** + * Get whether this comment is editable or not. + * @return {boolean} True if editable. + */ +Blockly.WorkspaceComment.prototype.isEditable = function() { + return this.editable_ && + !(this.workspace && this.workspace.options.readOnly); +}; + +/** + * Set whether this comment is editable or not. + * @param {boolean} editable True if editable. + */ +Blockly.WorkspaceComment.prototype.setEditable = function(editable) { + this.editable_ = editable; +}; + /** * Returns this comment's text. * @return {string} Comment text. diff --git a/core/workspace_comment_render_svg.js b/core/workspace_comment_render_svg.js index 74e43d23b..c2bc5f047 100644 --- a/core/workspace_comment_render_svg.js +++ b/core/workspace_comment_render_svg.js @@ -160,6 +160,7 @@ Blockly.WorkspaceCommentSvg.prototype.createEditor_ = function() { var textarea = document.createElementNS(Blockly.utils.dom.HTML_NS, 'textarea'); textarea.className = 'blocklyCommentTextarea'; textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); + textarea.readOnly = !this.isEditable(); body.appendChild(textarea); this.textarea_ = textarea; this.foreignObject_.appendChild(body); @@ -325,7 +326,7 @@ Blockly.WorkspaceCommentSvg.prototype.unbindDragEvents_ = function() { } }; -/* +/** * Handle a mouse-up event while dragging a comment's border or resize handle. * @param {!Event} e Mouse up event. * @private diff --git a/core/workspace_comment_svg.js b/core/workspace_comment_svg.js index 657bae7d8..a2fcef7cc 100644 --- a/core/workspace_comment_svg.js +++ b/core/workspace_comment_svg.js @@ -23,6 +23,7 @@ goog.provide('Blockly.WorkspaceCommentSvg'); +goog.require('Blockly.Css'); goog.require('Blockly.Events'); goog.require('Blockly.Events.CommentCreate'); goog.require('Blockly.Events.CommentDelete'); @@ -49,6 +50,21 @@ goog.require('Blockly.WorkspaceComment'); */ Blockly.WorkspaceCommentSvg = function(workspace, content, height, width, opt_id) { + + /** + * Mouse up event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseUpWrapper_ = null; + + /** + * Mouse move event data. + * @type {?Blockly.EventData} + * @private + */ + this.onMouseMoveWrapper_ = null; + // Create core elements for the block. /** * @type {SVGElement} @@ -170,9 +186,9 @@ Blockly.WorkspaceCommentSvg.prototype.pathMouseDown_ = function(e) { /** * Show the context menu for this workspace comment. * @param {!Event} e Mouse event. - * @private + * @package */ -Blockly.WorkspaceCommentSvg.prototype.showContextMenu_ = function(e) { +Blockly.WorkspaceCommentSvg.prototype.showContextMenu = function(e) { if (this.workspace.options.readOnly) { return; } @@ -343,9 +359,9 @@ Blockly.WorkspaceCommentSvg.prototype.translate = function(x, y) { * Move this comment to its workspace's drag surface, accounting for * positioning. Generally should be called at the same time as * setDragging(true). Does nothing if useDragSurface_ is false. - * @private + * @package */ -Blockly.WorkspaceCommentSvg.prototype.moveToDragSurface_ = function() { +Blockly.WorkspaceCommentSvg.prototype.moveToDragSurface = function() { if (!this.useDragSurface_) { return; } @@ -368,7 +384,7 @@ Blockly.WorkspaceCommentSvg.prototype.moveToDragSurface_ = function() { * on the workspace canvas, in workspace coordinates. * @private */ -Blockly.WorkspaceCommentSvg.prototype.moveOffDragSurface_ = function(newXY) { +Blockly.WorkspaceCommentSvg.prototype.moveOffDragSurface = function(newXY) { if (!this.useDragSurface_) { return; } @@ -467,6 +483,17 @@ Blockly.WorkspaceCommentSvg.prototype.setMovable = function(movable) { this.updateMovable(); }; +/** + * Set whether this comment is editable or not. + * @param {boolean} editable True if editable. + */ +Blockly.WorkspaceCommentSvg.prototype.setEditable = function(editable) { + Blockly.WorkspaceCommentSvg.superClass_.setEditable.call(this, editable); + if (this.textarea_) { + this.textarea_.readOnly = !editable; + } +}; + /** * Recursively adds or removes the dragging class to this node and its children. * @param {boolean} adding True if adding, false if removing. @@ -595,3 +622,68 @@ Blockly.WorkspaceCommentSvg.prototype.toXmlWithXY = function(opt_noId) { element.setAttribute('w', this.getWidth()); return element; }; + +/** + * CSS for workspace comment. See css.js for use. + */ +Blockly.Css.register([ + /* eslint-disable indent */ + '.blocklyCommentForeignObject {', + 'position: relative;', + 'z-index: 0;', + '}', + + '.blocklyCommentRect {', + 'fill: #E7DE8E;', + 'stroke: #bcA903;', + 'stroke-width: 1px', + '}', + + '.blocklyCommentTarget {', + 'fill: transparent;', + 'stroke: #bcA903;', + '}', + + '.blocklyCommentTargetFocused {', + 'fill: none;', + '}', + + '.blocklyCommentHandleTarget {', + 'fill: none;', + '}', + + '.blocklyCommentHandleTargetFocused {', + 'fill: transparent;', + '}', + + '.blocklyFocused>.blocklyCommentRect {', + 'fill: #B9B272;', + 'stroke: #B9B272;', + '}', + + '.blocklySelected>.blocklyCommentTarget {', + 'stroke: #fc3;', + 'stroke-width: 3px;', + '}', + + '.blocklyCommentDeleteIcon {', + 'cursor: pointer;', + 'fill: #000;', + 'display: none', + '}', + + '.blocklySelected > .blocklyCommentDeleteIcon {', + 'display: block', + '}', + + '.blocklyDeleteIconShape {', + 'fill: #000;', + 'stroke: #000;', + 'stroke-width: 1px;', + '}', + + '.blocklyDeleteIconShape.blocklyDeleteIconHighlighted {', + 'stroke: #fc3;', + '}' + /* eslint-enable indent */ +]); diff --git a/core/workspace_drag_surface_svg.js b/core/workspace_drag_surface_svg.js index 9e33f2ae7..70a5dd360 100644 --- a/core/workspace_drag_surface_svg.js +++ b/core/workspace_drag_surface_svg.js @@ -121,7 +121,7 @@ Blockly.WorkspaceDragSurfaceSvg.prototype.translateSurface = function(x, y) { * @package */ Blockly.WorkspaceDragSurfaceSvg.prototype.getSurfaceTranslation = function() { - return Blockly.utils.getRelativeXY(this.SVG_); + return Blockly.utils.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_)); }; /** @@ -136,8 +136,8 @@ Blockly.WorkspaceDragSurfaceSvg.prototype.clearAndHide = function(newSurface) { throw Error('Couldn\'t clear and hide the drag surface: missing ' + 'new surface.'); } - var blockCanvas = this.SVG_.childNodes[0]; - var bubbleCanvas = this.SVG_.childNodes[1]; + var blockCanvas = /** @type {!Element} */ (this.SVG_.childNodes[0]); + var bubbleCanvas = /** @type {!Element} */ (this.SVG_.childNodes[1]); if (!blockCanvas || !bubbleCanvas || !Blockly.utils.dom.hasClass(blockCanvas, 'blocklyBlockCanvas') || !Blockly.utils.dom.hasClass(bubbleCanvas, 'blocklyBubbleCanvas')) { diff --git a/core/workspace_dragger.js b/core/workspace_dragger.js index 67527322f..8d4d10568 100644 --- a/core/workspace_dragger.js +++ b/core/workspace_dragger.js @@ -46,7 +46,7 @@ Blockly.WorkspaceDragger = function(workspace) { * The scroll position of the workspace at the beginning of the drag. * Coordinate system: pixel coordinates. * @type {!Blockly.utils.Coordinate} - * @private + * @protected */ this.startScrollXY_ = new Blockly.utils.Coordinate( workspace.scrollX, workspace.scrollY); @@ -55,6 +55,7 @@ Blockly.WorkspaceDragger = function(workspace) { /** * Sever all links from this object. * @package + * @suppress {checkTypes} */ Blockly.WorkspaceDragger.prototype.dispose = function() { this.workspace_ = null; diff --git a/core/workspace_events.js b/core/workspace_events.js index 581366574..b3b690f25 100644 --- a/core/workspace_events.js +++ b/core/workspace_events.js @@ -24,7 +24,7 @@ goog.provide('Blockly.Events.FinishedLoading'); goog.require('Blockly.Events'); -goog.require('Blockly.Events.Abstract'); +goog.require('Blockly.Events.Ui'); goog.require('Blockly.utils.object'); @@ -46,7 +46,7 @@ Blockly.Events.FinishedLoading = function(workspace) { this.workspaceId = workspace.id; /** - * The event group id for the group this event belongs to. Groups define + * The event group ID for the group this event belongs to. Groups define * events that should be treated as an single action from the user's * perspective, and should be undone together. * @type {string} @@ -57,7 +57,7 @@ Blockly.Events.FinishedLoading = function(workspace) { this.recordUndo = false; }; Blockly.utils.object.inherits(Blockly.Events.FinishedLoading, - Blockly.Events.Abstract); + Blockly.Events.Ui); /** * Type of this event. diff --git a/core/workspace_svg.js b/core/workspace_svg.js index fd0de41bb..5e72f231a 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -31,8 +31,12 @@ goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockCreate'); goog.require('Blockly.Gesture'); goog.require('Blockly.Grid'); +goog.require('Blockly.MarkerManager'); goog.require('Blockly.Msg'); +goog.require('Blockly.navigation'); goog.require('Blockly.Options'); +goog.require('Blockly.ThemeManager'); +goog.require('Blockly.Themes.Classic'); goog.require('Blockly.TouchGesture'); goog.require('Blockly.utils'); goog.require('Blockly.utils.Coordinate'); @@ -44,6 +48,8 @@ goog.require('Blockly.WorkspaceAudio'); goog.require('Blockly.WorkspaceDragSurfaceSvg'); goog.require('Blockly.Xml'); +goog.requireType('Blockly.blockRendering.Renderer'); + /** * Class for a workspace. This is an onscreen area with optional trashcan, @@ -59,8 +65,10 @@ goog.require('Blockly.Xml'); Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface) { Blockly.WorkspaceSvg.superClass_.constructor.call(this, options); + /** @type {function():!Object} */ this.getMetrics = options.getMetrics || Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_; + /** @type {function(!Object):void} */ this.setMetrics = options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_; @@ -75,12 +83,12 @@ Blockly.WorkspaceSvg = function(options, } this.useWorkspaceDragSurface_ = - this.workspaceDragSurface_ && Blockly.utils.is3dSupported(); + !!this.workspaceDragSurface_ && Blockly.utils.is3dSupported(); /** * List of currently highlighted blocks. Block highlighting is often used to * visually mark blocks currently being executed. - * @type !Array. + * @type {!Array.} * @private */ this.highlightedBlocks_ = []; @@ -90,7 +98,8 @@ Blockly.WorkspaceSvg = function(options, * @type {!Blockly.WorkspaceAudio} * @private */ - this.audioManager_ = new Blockly.WorkspaceAudio(options.parentWorkspace); + this.audioManager_ = new Blockly.WorkspaceAudio( + /** @type {Blockly.WorkspaceSvg} */ (options.parentWorkspace)); /** * This workspace's grid object or null. @@ -101,20 +110,11 @@ Blockly.WorkspaceSvg = function(options, new Blockly.Grid(options.gridPattern, options.gridOptions) : null; /** - * Holds the cursors svg element when the cursor is attached to the workspace. - * This is null if there is no cursor on the workspace. - * @type {SVGElement} + * Manager in charge of markers and cursors. + * @type {!Blockly.MarkerManager} * @private */ - this.cursorSvg_ = null; - - /** - * Holds the markers svg element when the marker is attached to the workspace. - * This is null if there is no marker on the workspace. - * @type {SVGElement} - * @private - */ - this.markerSvg_ = null; + this.markerManager_ = new Blockly.MarkerManager(this); if (Blockly.Variables && Blockly.Variables.flyoutCategory) { this.registerToolboxCategoryCallback(Blockly.VARIABLE_CATEGORY_NAME, @@ -127,14 +127,42 @@ Blockly.WorkspaceSvg = function(options, if (Blockly.Procedures && Blockly.Procedures.flyoutCategory) { this.registerToolboxCategoryCallback(Blockly.PROCEDURE_CATEGORY_NAME, Blockly.Procedures.flyoutCategory); + this.addChangeListener(Blockly.Procedures.mutatorOpenListener); } + /** + * Object in charge of storing and updating the workspace theme. + * @type {!Blockly.ThemeManager} + * @protected + */ + this.themeManager_ = this.options.parentWorkspace ? + this.options.parentWorkspace.getThemeManager() : + new Blockly.ThemeManager(this, + this.options.theme || Blockly.Themes.Classic); + /** * The block renderer used for rendering blocks on this workspace. * @type {!Blockly.blockRendering.Renderer} * @private */ this.renderer_ = Blockly.blockRendering.init(this.options.renderer || 'geras'); + + /** + * Cached parent SVG. + * @type {SVGElement} + * @private + */ + this.cachedParentSvg_ = null; + + this.themeManager_.subscribeWorkspace(this); + this.renderer_.getConstants().refreshTheme(this.getTheme()); + + /** + * True if keyboard accessibility mode is on, false otherwise. + * @type {boolean} + * @package + */ + this.keyboardAccessibilityMode = false; }; Blockly.utils.object.inherits(Blockly.WorkspaceSvg, Blockly.Workspace); @@ -142,6 +170,7 @@ Blockly.utils.object.inherits(Blockly.WorkspaceSvg, Blockly.Workspace); * A wrapper function called when a resize event occurs. * You can pass the result to `unbindEvent_`. * @type {Array.} + * @private */ Blockly.WorkspaceSvg.prototype.resizeHandlerWrapper_ = null; @@ -344,7 +373,7 @@ Blockly.WorkspaceSvg.prototype.isDragSurfaceActive_ = false; /** * The first parent div with 'injectionDiv' in the name, or null if not set. * Access this with getInjectionDiv. - * @type {!Element} + * @type {Element} * @private */ Blockly.WorkspaceSvg.prototype.injectionDiv_ = null; @@ -353,7 +382,7 @@ Blockly.WorkspaceSvg.prototype.injectionDiv_ = null; * Last known position of the page scroll. * This is used to determine whether we have recalculated screen coordinate * stuff since the page scrolled. - * @type {!Blockly.utils.Coordinate} + * @type {Blockly.utils.Coordinate} * @private */ Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null; @@ -361,7 +390,7 @@ Blockly.WorkspaceSvg.prototype.lastRecordedPageScroll_ = null; /** * Map from function names to callbacks, for deciding what to do when a button * is clicked. - * @type {!Object.} + * @type {!Object.} * @private */ Blockly.WorkspaceSvg.prototype.flyoutButtonCallbacks_ = {}; @@ -369,7 +398,7 @@ Blockly.WorkspaceSvg.prototype.flyoutButtonCallbacks_ = {}; /** * Map from function names to callbacks, for deciding what to do when a custom * toolbox category is opened. - * @type {!Object.>} + * @type {!Object.>} * @private */ Blockly.WorkspaceSvg.prototype.toolboxCategoryCallbacks_ = {}; @@ -379,7 +408,7 @@ Blockly.WorkspaceSvg.prototype.toolboxCategoryCallbacks_ = {}; * workspace's context menu or edit the workspace-created set of menu options. * @param {!Array.} options List of menu options to add to. */ -Blockly.WorkspaceSvg.prototype.configureContextMenu = null; +Blockly.WorkspaceSvg.prototype.configureContextMenu; /** * In a flyout, the target workspace where blocks should be placed after a drag. @@ -404,45 +433,11 @@ Blockly.WorkspaceSvg.prototype.inverseScreenCTM_ = null; Blockly.WorkspaceSvg.prototype.inverseScreenCTMDirty_ = true; /** - * Get the block renderer attached to this workspace. - * @return {!Blockly.blockRendering.Renderer} The renderer attached to this workspace. + * Get the marker manager for this workspace. + * @return {Blockly.MarkerManager} The marker manager. */ -Blockly.WorkspaceSvg.prototype.getRenderer = function() { - return this.renderer_; -}; - -/** - * Sets the cursor for use with keyboard navigation. - * - * @param {Blockly.Cursor} cursor The cursor used to move around this workspace. - * @override - */ -Blockly.WorkspaceSvg.prototype.setCursor = function(cursor) { - if (this.cursor_ && this.cursor_.getDrawer()) { - this.cursor_.getDrawer().dispose(); - } - this.cursor_ = cursor; - if (this.cursor_) { - this.cursor_.setDrawer(this.getRenderer().makeCursorDrawer(this, false)); - this.setCursorSvg(this.cursor_.getDrawer().createDom()); - } -}; - -/** - * Sets the marker for use with keyboard navigation. - * @param {Blockly.MarkerCursor} marker The immovable cursor used to mark a - * location on the workspace. - * @override - */ -Blockly.WorkspaceSvg.prototype.setMarker = function(marker) { - if (this.marker_ && this.marker_.getDrawer()) { - this.marker_.getDrawer().dispose(); - } - this.marker_ = marker; - if (this.marker_) { - this.marker_.setDrawer(this.getRenderer().makeCursorDrawer(this, true)); - this.setMarkerSvg(this.marker_.getDrawer().createDom()); - } +Blockly.WorkspaceSvg.prototype.getMarkerManager = function() { + return this.markerManager_; }; /** @@ -452,15 +447,7 @@ Blockly.WorkspaceSvg.prototype.setMarker = function(marker) { * @package */ Blockly.WorkspaceSvg.prototype.setCursorSvg = function(cursorSvg) { - if (!cursorSvg) { - this.cursorSvg_ = null; - return; - } - - if (this.svgBlockCanvas_) { - this.svgBlockCanvas_.appendChild(cursorSvg); - this.cursorSvg_ = cursorSvg; - } + this.markerManager_.setCursorSvg(cursorSvg); }; /** @@ -470,18 +457,111 @@ Blockly.WorkspaceSvg.prototype.setCursorSvg = function(cursorSvg) { * @package */ Blockly.WorkspaceSvg.prototype.setMarkerSvg = function(markerSvg) { - if (!markerSvg) { - this.markerSvg_ = null; - return; + this.markerManager_.setMarkerSvg(markerSvg); +}; + +/** + * Get the marker with the given id. + * @param {string} id The id of the marker. + * @return {Blockly.Marker} The marker with the given id or null if no marker + * with the given id exists. + * @package + */ +Blockly.WorkspaceSvg.prototype.getMarker = function(id) { + if (this.markerManager_) { + return this.markerManager_.getMarker(id); + } + return null; +}; + +/** + * The cursor for this workspace. + * @return {Blockly.Cursor} The cursor for the workspace. + */ +Blockly.WorkspaceSvg.prototype.getCursor = function() { + if (this.markerManager_) { + return this.markerManager_.getCursor(); + } + return null; +}; + +/** + * Get the block renderer attached to this workspace. + * @return {!Blockly.blockRendering.Renderer} The renderer attached to this workspace. + */ +Blockly.WorkspaceSvg.prototype.getRenderer = function() { + return this.renderer_; +}; + +/** + * Get the theme manager for this workspace. + * @return {!Blockly.ThemeManager} The theme manager for this workspace. + * @package + */ +Blockly.WorkspaceSvg.prototype.getThemeManager = function() { + return this.themeManager_; +}; + +/** + * Get the workspace theme object. + * @return {!Blockly.Theme} The workspace theme object. + */ +Blockly.WorkspaceSvg.prototype.getTheme = function() { + return this.themeManager_.getTheme(); +}; + +/** + * Set the workspace theme object. + * If no theme is passed, default to the `Blockly.Themes.Classic` theme. + * @param {Blockly.Theme} theme The workspace theme object. + */ +Blockly.WorkspaceSvg.prototype.setTheme = function(theme) { + if (!theme) { + theme = /** @type {!Blockly.Theme} */ (Blockly.Themes.Classic); + } + this.themeManager_.setTheme(theme); +}; + +/** + * Refresh all blocks on the workspace after a theme update. + * @package + */ +Blockly.WorkspaceSvg.prototype.refreshTheme = function() { + this.getRenderer().getConstants().refreshTheme(this.getTheme()); + + // Update all blocks in workspace that have a style name. + this.updateBlockStyles_(this.getAllBlocks(false).filter( + function(block) { + return block.getStyleName() !== undefined; + } + )); + + // Update current toolbox selection. + this.refreshToolboxSelection(); + if (this.toolbox_) { + this.toolbox_.updateColourFromTheme(); } - if (this.svgBlockCanvas_) { - if (this.cursorSvg_) { - this.svgBlockCanvas_.insertBefore(markerSvg, this.cursorSvg_); - } else { - this.svgBlockCanvas_.appendChild(markerSvg); + var event = new Blockly.Events.Ui(null, 'theme', null, null); + event.workspaceId = this.id; + Blockly.Events.fire(event); +}; + +/** + * Updates all the blocks with new style. + * @param {!Array.} blocks List of blocks to update the style + * on. + * @private + */ +Blockly.WorkspaceSvg.prototype.updateBlockStyles_ = function(blocks) { + for (var i = 0, block; (block = blocks[i]); i++) { + var blockStyleName = block.getStyleName(); + if (blockStyleName) { + block.setStyle(blockStyleName); + if (block.mutator) { + block.mutator.updateBlockStyle(); + } } - this.markerSvg_ = markerSvg; } }; @@ -524,9 +604,9 @@ Blockly.WorkspaceSvg.prototype.isVisible = function() { * Return the absolute coordinates of the top-left corner of this element, * scales that after canvas SVG element, if it's a descendant. * The origin (0,0) is the top-left corner of the Blockly SVG. - * @param {!Element} element Element to find the coordinates of. + * @param {!SVGElement} element SVG element to find the coordinates of. * @return {!Blockly.utils.Coordinate} Object with .x and .y properties. - * @private + * @package */ Blockly.WorkspaceSvg.prototype.getSvgXY = function(element) { var x = 0; @@ -547,7 +627,7 @@ Blockly.WorkspaceSvg.prototype.getSvgXY = function(element) { } x += xy.x * scale; y += xy.y * scale; - element = element.parentNode; + element = /** @type {!SVGElement} */ (element.parentNode); } while (element && element != this.getParentSvg()); return new Blockly.utils.Coordinate(x, y); }; @@ -561,12 +641,14 @@ Blockly.WorkspaceSvg.prototype.getSvgXY = function(element) { * @package */ Blockly.WorkspaceSvg.prototype.getOriginOffsetInPixels = function() { - return Blockly.utils.getInjectionDivXY_(this.svgBlockCanvas_); + return Blockly.utils.getInjectionDivXY_(this.getCanvas()); }; /** * Return the injection div that is a parent of this workspace. * Walks the DOM the first time it's called, then returns a cached value. + * Note: We assume this is only called after the workspace has been injected + * into the DOM. * @return {!Element} The first parent div with 'injectionDiv' in the name. * @package */ @@ -581,10 +663,19 @@ Blockly.WorkspaceSvg.prototype.getInjectionDiv = function() { this.injectionDiv_ = element; break; } - element = element.parentNode; + element = /** @type {!Element} */ (element.parentNode); } } - return this.injectionDiv_; + return /** @type {!Element} */ (this.injectionDiv_); +}; + +/** + * Get the svg block canvas for the workspace. + * @return {SVGElement} The svg group for the workspace. + * @package + */ +Blockly.WorkspaceSvg.prototype.getBlockCanvas = function() { + return this.svgBlockCanvas_; }; /** @@ -627,7 +718,8 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) { this.svgBackground_.style.fill = 'url(#' + this.grid_.getPatternId() + ')'; } else { - this.themeManager_.subscribe(this.svgBackground_, 'workspace', 'fill'); + this.themeManager_.subscribe(this.svgBackground_, + 'workspaceBackgroundColour', 'fill'); } } /** @type {SVGElement} */ @@ -657,20 +749,20 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) { } this.recordDeleteAreas(); - this.cursor_.setDrawer(this.getRenderer().makeCursorDrawer(this, false)); - var svgCursor = this.cursor_.getDrawer().createDom(); - this.svgGroup_.appendChild(svgCursor); - - this.marker_.setDrawer(this.getRenderer().makeCursorDrawer(this, true)); - var svgMarker = this.marker_.getDrawer().createDom(); - this.svgGroup_.appendChild(svgMarker); + this.markerManager_.setCursor(new Blockly.Cursor()); + this.markerManager_.registerMarker(Blockly.navigation.MARKER_NAME, + new Blockly.Marker()); + var constants = this.getRenderer().getConstants(); + constants.injectCSS(this.getRenderer().name); + constants.createDom(this.svgGroup_); return this.svgGroup_; }; /** * Dispose of this workspace. * Unlink from all DOM elements to prevent memory leaks. + * @suppress {checkTypes} */ Blockly.WorkspaceSvg.prototype.dispose = function() { // Stop rerendering. @@ -705,14 +797,6 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { this.zoomControls_ = null; } - if (this.marker_) { - this.marker_.getDrawer().dispose(); - } - - if (this.getCursor()) { - this.getCursor().getDrawer().dispose(); - } - if (this.audioManager_) { this.audioManager_.dispose(); this.audioManager_ = null; @@ -723,9 +807,22 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { this.grid_ = null; } + this.renderer_.getConstants().dispose(); + if (this.themeManager_) { + this.themeManager_.unsubscribeWorkspace(this); this.themeManager_.unsubscribe(this.svgBackground_); + if (!this.options.parentWorkspace) { + this.themeManager_.dispose(); + this.themeManager_ = null; + } } + + if (this.markerManager_) { + this.markerManager_.dispose(); + this.markerManager_ = null; + } + Blockly.WorkspaceSvg.superClass_.dispose.call(this); this.connectionDBList = null; @@ -792,18 +889,19 @@ Blockly.WorkspaceSvg.prototype.addZoomControls = function() { * Add a flyout element in an element with the given tag name. * @param {string} tagName What type of tag the flyout belongs in. * @return {!Element} The element containing the flyout DOM. - * @private + * @package */ -Blockly.WorkspaceSvg.prototype.addFlyout_ = function(tagName) { - var workspaceOptions = { - disabledPatternId: this.options.disabledPatternId, - parentWorkspace: this, - RTL: this.RTL, - oneBasedIndex: this.options.oneBasedIndex, - horizontalLayout: this.horizontalLayout, - toolboxPosition: this.options.toolboxPosition, - renderer: this.options.renderer - }; +Blockly.WorkspaceSvg.prototype.addFlyout = function(tagName) { + var workspaceOptions = new Blockly.Options( + /** @type {!Blockly.BlocklyOptions} */ + ({ + 'parentWorkspace': this, + 'rtl': this.RTL, + 'oneBasedIndex': this.options.oneBasedIndex, + 'horizontalLayout': this.horizontalLayout, + 'renderer': this.options.renderer + })); + workspaceOptions.toolboxPosition = this.options.toolboxPosition; if (this.horizontalLayout) { if (!Blockly.HorizontalFlyout) { throw Error('Missing require for Blockly.HorizontalFlyout'); @@ -827,15 +925,16 @@ Blockly.WorkspaceSvg.prototype.addFlyout_ = function(tagName) { * Getter for the flyout associated with this workspace. This flyout may be * owned by either the toolbox or the workspace, depending on toolbox * configuration. It will be null if there is no flyout. + * @param {boolean=} opt_own Only return the workspace's own flyout if True. * @return {Blockly.Flyout} The flyout on this workspace. * @package */ -Blockly.WorkspaceSvg.prototype.getFlyout = function() { - if (this.flyout_) { +Blockly.WorkspaceSvg.prototype.getFlyout = function(opt_own) { + if (this.flyout_ || opt_own) { return this.flyout_; } if (this.toolbox_) { - return this.toolbox_.flyout_; + return this.toolbox_.getFlyout(); } return null; }; @@ -870,9 +969,7 @@ Blockly.WorkspaceSvg.prototype.resizeContents = function() { return; } if (this.scrollbar) { - var metrics = this.getMetrics(); - this.scrollbar.hScroll.resizeContentHorizontal(metrics); - this.scrollbar.vScroll.resizeContentVertical(metrics); + this.scrollbar.resize(); } this.updateInverseScreenCTM(); }; @@ -921,37 +1018,38 @@ Blockly.WorkspaceSvg.prototype.updateScreenCalculationsIfScrolled = /** * Get the SVG element that forms the drawing surface. - * @return {!SVGElement} SVG element. + * @return {!SVGGElement} SVG group element. */ Blockly.WorkspaceSvg.prototype.getCanvas = function() { - return this.svgBlockCanvas_; + return /** @type {!SVGGElement} */ (this.svgBlockCanvas_); }; /** * Get the SVG element that forms the bubble surface. - * @return {!SVGGElement} SVG element. + * @return {!SVGGElement} SVG group element. */ Blockly.WorkspaceSvg.prototype.getBubbleCanvas = function() { - return this.svgBubbleCanvas_; + return /** @type {!SVGGElement} */ (this.svgBubbleCanvas_); }; /** * Get the SVG element that contains this workspace. - * @return {SVGElement} SVG element. + * Note: We assume this is only called after the workspace has been injected + * into the DOM. + * @return {!SVGElement} SVG element. */ Blockly.WorkspaceSvg.prototype.getParentSvg = function() { - if (this.cachedParentSvg_) { - return this.cachedParentSvg_; - } - var element = this.svgGroup_; - while (element) { - if (element.tagName == 'svg') { - this.cachedParentSvg_ = element; - return element; + if (!this.cachedParentSvg_) { + var element = this.svgGroup_; + while (element) { + if (element.tagName == 'svg') { + this.cachedParentSvg_ = element; + break; + } + element = /** @type {!SVGElement} */ (element.parentNode); } - element = /** @type {!SVGElement} */ (element.parentNode); } - return null; + return /** @type {!SVGElement} */ (this.cachedParentSvg_); }; /** @@ -1027,12 +1125,13 @@ Blockly.WorkspaceSvg.prototype.setupDragSurface = function() { // Figure out where we want to put the canvas back. The order // in the is important because things are layered. - var previousElement = this.svgBlockCanvas_.previousSibling; + var previousElement = + /** @type {Element} */ (this.svgBlockCanvas_.previousSibling); var width = parseInt(this.getParentSvg().getAttribute('width'), 10); var height = parseInt(this.getParentSvg().getAttribute('height'), 10); - var coord = Blockly.utils.getRelativeXY(this.svgBlockCanvas_); - this.workspaceDragSurface_.setContentsAndShow(this.svgBlockCanvas_, - this.svgBubbleCanvas_, previousElement, width, height, this.scale); + var coord = Blockly.utils.getRelativeXY(this.getCanvas()); + this.workspaceDragSurface_.setContentsAndShow(this.getCanvas(), + this.getBubbleCanvas(), previousElement, width, height, this.scale); this.workspaceDragSurface_.translateSurface(coord.x, coord.y); }; @@ -1137,7 +1236,7 @@ Blockly.WorkspaceSvg.prototype.traceOn = function() { Blockly.WorkspaceSvg.prototype.highlightBlock = function(id, opt_state) { if (opt_state === undefined) { // Unhighlight all blocks. - for (var i = 0, block; block = this.highlightedBlocks_[i]; i++) { + for (var i = 0, block; (block = this.highlightedBlocks_[i]); i++) { block.setHighlighted(false); } this.highlightedBlocks_.length = 0; @@ -1178,6 +1277,7 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) { /** * Paste the provided block onto the workspace. * @param {!Element} xmlBlock XML block element. + * @private */ Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) { Blockly.Events.disable(); @@ -1185,9 +1285,12 @@ Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) { var block = Blockly.Xml.domToBlock(xmlBlock, this); // Handle paste for keyboard navigation - var markedNode = this.getMarker().getCurNode(); - if (Blockly.keyboardAccessibilityMode && markedNode) { - Blockly.navigation.insertBlock(block, markedNode.getLocation()); + var markedNode = this.getMarker(Blockly.navigation.MARKER_NAME).getCurNode(); + if (this.keyboardAccessibilityMode && markedNode && + markedNode.isConnection()) { + var markedLocation = + /** @type {!Blockly.Connection} */ (markedNode.getLocation()); + Blockly.navigation.insertBlock(block, markedLocation); return; } @@ -1203,7 +1306,7 @@ Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) { do { var collide = false; var allBlocks = this.getAllBlocks(false); - for (var i = 0, otherBlock; otherBlock = allBlocks[i]; i++) { + for (var i = 0, otherBlock; (otherBlock = allBlocks[i]); i++) { var otherXY = otherBlock.getRelativeToSurfaceXY(); if (Math.abs(blockX - otherXY.x) <= 1 && Math.abs(blockY - otherXY.y) <= 1) { @@ -1214,7 +1317,7 @@ Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) { if (!collide) { // Check for blocks in snap range to any of its connections. var connections = block.getConnections_(false); - for (var i = 0, connection; connection = connections[i]; i++) { + for (var i = 0, connection; (connection = connections[i]); i++) { var neighbour = connection.closest(Blockly.SNAP_RADIUS, new Blockly.utils.Coordinate(blockX, blockY)); if (neighbour.connection) { @@ -1247,6 +1350,8 @@ Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) { * Paste the provided comment onto the workspace. * @param {!Element} xmlComment XML workspace comment element. * @private + * @suppress {checkTypes} Suppress checks while workspace comments are not + * bundled in. */ Blockly.WorkspaceSvg.prototype.pasteWorkspaceComment_ = function(xmlComment) { Blockly.Events.disable(); @@ -1281,7 +1386,7 @@ Blockly.WorkspaceSvg.prototype.pasteWorkspaceComment_ = function(xmlComment) { */ Blockly.WorkspaceSvg.prototype.refreshToolboxSelection = function() { var ws = this.isFlyout ? this.targetWorkspace : this; - if (ws && !ws.currentGesture_ && ws.toolbox_ && ws.toolbox_.flyout_) { + if (ws && !ws.currentGesture_ && ws.toolbox_ && ws.toolbox_.getFlyout()) { ws.toolbox_.refreshSelection(); } }; @@ -1291,7 +1396,6 @@ Blockly.WorkspaceSvg.prototype.refreshToolboxSelection = function() { * flyout to show the renamed variable immediately. * @param {string} id ID of the variable to rename. * @param {string} newName New variable name. - * @package */ Blockly.WorkspaceSvg.prototype.renameVariableById = function(id, newName) { Blockly.WorkspaceSvg.superClass_.renameVariableById.call(this, id, newName); @@ -1302,7 +1406,6 @@ Blockly.WorkspaceSvg.prototype.renameVariableById = function(id, newName) { * Delete a variable by the passed in ID. Update the flyout to show * immediately that the variable is deleted. * @param {string} id ID of variable to delete. - * @package */ Blockly.WorkspaceSvg.prototype.deleteVariableById = function(id) { Blockly.WorkspaceSvg.superClass_.deleteVariableById.call(this, id); @@ -1313,13 +1416,12 @@ Blockly.WorkspaceSvg.prototype.deleteVariableById = function(id) { * Create a new variable with the given name. Update the flyout to show the * new variable immediately. * @param {string} name The new variable's name. - * @param {string=} opt_type The type of the variable like 'int' or 'string'. + * @param {?string=} opt_type The type of the variable like 'int' or 'string'. * Does not need to be unique. Field_variable can filter variables based on * their type. This will default to '' which is a specific type. - * @param {string=} opt_id The unique ID of the variable. This will default to + * @param {?string=} opt_id The unique ID of the variable. This will default to * a UUID. - * @return {Blockly.VariableModel} The newly created variable. - * @package + * @return {!Blockly.VariableModel} The newly created variable. */ Blockly.WorkspaceSvg.prototype.createVariable = function(name, opt_type, opt_id) { @@ -1403,7 +1505,8 @@ Blockly.WorkspaceSvg.prototype.moveDrag = function(e) { // Fix scale of mouse event. point.x /= this.scale; point.y /= this.scale; - return Blockly.utils.Coordinate.sum(this.dragDeltaXY_, point); + return Blockly.utils.Coordinate.sum( + /** @type {!Blockly.utils.Coordinate} */ (this.dragDeltaXY_), point); }; /** @@ -1434,7 +1537,8 @@ Blockly.WorkspaceSvg.prototype.isContentBounded = function() { (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.wheel) || + (this.options.zoomOptions && this.options.zoomOptions.pinch); }; /** @@ -1442,17 +1546,17 @@ Blockly.WorkspaceSvg.prototype.isContentBounded = function() { * * This means the user can reposition the X Y coordinates of the workspace * through input. This can be through scrollbars, scroll wheel, dragging, or - * through zooming with the scroll wheel (since the zoom is centered on the - * mouse position). This does not include zooming with the zoom controls + * through zooming with the scroll wheel or pinch (since the zoom is centered on + * the mouse position). This does not include zooming with the zoom controls * since the X Y coordinates are decided programmatically. * @return {boolean} True if the workspace is movable, false otherwise. - * @package */ 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.wheel) || + (this.options.zoomOptions && this.options.zoomOptions.pinch); }; /** @@ -1546,7 +1650,7 @@ Blockly.WorkspaceSvg.prototype.cleanUp = function() { Blockly.Events.setGroup(true); var topBlocks = this.getTopBlocks(true); var cursorY = 0; - for (var i = 0, block; block = topBlocks[i]; i++) { + for (var i = 0, block; (block = topBlocks[i]); i++) { if (!block.isMovable()) { continue; } @@ -1554,7 +1658,8 @@ Blockly.WorkspaceSvg.prototype.cleanUp = function() { block.moveBy(-xy.x, cursorY - xy.y); block.snapToGrid(); cursorY = block.getRelativeToSurfaceXY().y + - block.getHeightWidth().height + Blockly.BlockSvg.MIN_BLOCK_Y; + block.getHeightWidth().height + + this.renderer_.getConstants().MIN_BLOCK_HEIGHT; } Blockly.Events.setGroup(false); this.setResizesEnabled(true); @@ -1563,9 +1668,9 @@ Blockly.WorkspaceSvg.prototype.cleanUp = function() { /** * Show the context menu for the workspace. * @param {!Event} e Mouse event. - * @private + * @package */ -Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) { +Blockly.WorkspaceSvg.prototype.showContextMenu = function(e) { if (this.options.readOnly || this.isFlyout) { return; } @@ -1765,7 +1870,7 @@ Blockly.WorkspaceSvg.prototype.setBrowserFocus = function() { } try { // Focus the workspace SVG - this is for Chrome and Firefox. - this.getParentSvg().focus(); + this.getParentSvg().focus({preventScroll:true}); } catch (e) { // IE and Edge do not support focus on SVG elements. When that fails // above, get the injectionDiv (the workspace's parent) and focus that @@ -1777,7 +1882,7 @@ Blockly.WorkspaceSvg.prototype.setBrowserFocus = function() { } catch (e) { // setActive support was discontinued in Edge so when that fails, call // focus instead. - this.getParentSvg().parentNode.focus(); + this.getParentSvg().parentNode.focus({preventScroll:true}); } } }; @@ -1795,12 +1900,6 @@ Blockly.WorkspaceSvg.prototype.setBrowserFocus = function() { * amount values zoom in. */ Blockly.WorkspaceSvg.prototype.zoom = function(x, y, amount) { - // TODO (#2782): Consider removing once pinch understands zoom configuration - // Mutators and flyouts don't support zooming, and pinch doesn't understand - // that. - if (this.isFlyout || this.isMutator) { - return; - } // Scale factor. var speed = this.options.zoomOptions.scaleSpeed; var scaleChange = Math.pow(speed, amount); @@ -1887,9 +1986,9 @@ Blockly.WorkspaceSvg.prototype.zoomToFit = function() { // the flyout, and the area we want to fit them includes the portion of // the workspace that is behind the flyout. if (this.horizontalLayout) { - workspaceHeight += this.flyout_.height_; + workspaceHeight += this.flyout_.getHeight(); // Convert from pixels to workspace coordinates. - blocksHeight += this.flyout_.height_ / this.scale; + blocksHeight += this.flyout_.getHeight() / this.scale; } else { workspaceWidth += this.flyout_.getWidth(); // Convert from pixels to workspace coordinates. @@ -1963,7 +2062,7 @@ Blockly.WorkspaceSvg.prototype.centerOnBlock = function(id) { return; } - var block = this.getBlockById(id); + var block = id ? this.getBlockById(id) : null; if (!block) { return; } @@ -2251,7 +2350,7 @@ Blockly.WorkspaceSvg.getContentDimensionsBounded_ = function(ws, svgSize) { * @return {!Object} Contains size and position metrics of a top level * workspace. * @private - * @this Blockly.WorkspaceSvg + * @this {Blockly.WorkspaceSvg} */ Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() { @@ -2333,7 +2432,7 @@ Blockly.WorkspaceSvg.getTopLevelWorkspaceMetrics_ = function() { * @param {!Object} xyRatio Contains an x and/or y property which is a float * between 0 and 1 specifying the degree of scrolling. * @private - * @this Blockly.WorkspaceSvg + * @this {Blockly.WorkspaceSvg} */ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) { var metrics = this.getMetrics(); @@ -2351,6 +2450,28 @@ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) { this.translate(x, y); }; +/** + * Find the block on this workspace with the specified ID. + * @param {string} id ID of block to find. + * @return {Blockly.BlockSvg} The sought after block, or null if not found. + * @override + */ +Blockly.WorkspaceSvg.prototype.getBlockById = function(id) { + return /** @type {Blockly.BlockSvg} */ ( + Blockly.WorkspaceSvg.superClass_.getBlockById.call(this, id)); +}; + +/** + * Finds the top-level blocks and returns them. Blocks are optionally sorted + * by position; top to bottom (with slight LTR or RTL bias). + * @param {boolean} ordered Sort the list if true. + * @return {!Array.} The top-level block objects. + * @override + */ +Blockly.WorkspaceSvg.prototype.getTopBlocks = function(ordered) { + return Blockly.WorkspaceSvg.superClass_.getTopBlocks.call(this, ordered); +}; + /** * Update whether this workspace has resizes enabled. * If enabled, workspace will resize when appropriate. @@ -2518,18 +2639,3 @@ Blockly.WorkspaceSvg.prototype.getAudioManager = function() { Blockly.WorkspaceSvg.prototype.getGrid = function() { return this.grid_; }; - -/** - * Refresh all blocks on the workspace, toolbox and flyout after a theme update. - * @package - * @override - */ -Blockly.WorkspaceSvg.prototype.refreshTheme = function() { - Blockly.WorkspaceSvg.superClass_.refreshTheme.call(this); - - // Update current toolbox selection. - this.refreshToolboxSelection(); - if (this.toolbox_) { - this.toolbox_.updateColourFromTheme(); - } -}; diff --git a/core/xml.js b/core/xml.js index e9b37bde9..a1a4dd970 100644 --- a/core/xml.js +++ b/core/xml.js @@ -41,7 +41,7 @@ goog.require('Blockly.utils.xml'); * Encode a block tree as XML. * @param {!Blockly.Workspace} workspace The workspace containing blocks. * @param {boolean=} opt_noId True if the encoder should skip the block IDs. - * @return {!Element} XML document. + * @return {!Element} XML DOM element. */ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) { var xml = Blockly.utils.xml.createElement('xml'); @@ -51,11 +51,11 @@ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) { xml.appendChild(variablesElement); } var comments = workspace.getTopComments(true); - for (var i = 0, comment; comment = comments[i]; i++) { + for (var i = 0, comment; (comment = comments[i]); i++) { xml.appendChild(comment.toXmlWithXY(opt_noId)); } var blocks = workspace.getTopBlocks(true); - for (var i = 0, block; block = blocks[i]; i++) { + for (var i = 0, block; (block = blocks[i]); i++) { xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId)); } return xml; @@ -65,11 +65,11 @@ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) { * Encode a list of variables as XML. * @param {!Array.} variableList List of all variable * models. - * @return {!Element} List of XML elements. + * @return {!Element} Tree of XML elements. */ Blockly.Xml.variablesToDom = function(variableList) { var variables = Blockly.utils.xml.createElement('variables'); - for (var i = 0, variable; variable = variableList[i]; i++) { + for (var i = 0, variable; (variable = variableList[i]); i++) { var element = Blockly.utils.xml.createElement('variable'); element.appendChild(Blockly.utils.xml.createTextNode(variable.name)); if (variable.type) { @@ -110,7 +110,7 @@ Blockly.Xml.blockToDomWithXY = function(block, opt_noId) { Blockly.Xml.fieldToDom_ = function(field) { if (field.isSerializable()) { var container = Blockly.utils.xml.createElement('field'); - container.setAttribute('name', field.name); + container.setAttribute('name', field.name || ''); return field.toXml(container); } return null; @@ -125,8 +125,8 @@ Blockly.Xml.fieldToDom_ = function(field) { * @private */ Blockly.Xml.allFieldsToDom_ = function(block, element) { - for (var i = 0, input; input = block.inputList[i]; i++) { - for (var j = 0, field; field = input.fieldRow[j]; j++) { + for (var i = 0, input; (input = block.inputList[i]); i++) { + for (var j = 0, field; (field = input.fieldRow[j]); j++) { var fieldDom = Blockly.Xml.fieldToDom_(field); if (fieldDom) { element.appendChild(fieldDom); @@ -146,7 +146,9 @@ Blockly.Xml.blockToDom = function(block, opt_noId) { Blockly.utils.xml.createElement(block.isShadow() ? 'shadow' : 'block'); element.setAttribute('type', block.type); if (!opt_noId) { - element.id = block.id; + // It's important to use setAttribute here otherwise IE11 won't serialize + // the block's id when domToText is called. + element.setAttribute('id', block.id); } if (block.mutationToDom) { // Custom data for an advanced block. @@ -178,7 +180,7 @@ Blockly.Xml.blockToDom = function(block, opt_noId) { element.appendChild(dataElement); } - for (var i = 0, input; input = block.inputList[i]; i++) { + for (var i = 0, input; (input = block.inputList[i]); i++) { var container; var empty = true; if (input.type == Blockly.DUMMY_INPUT) { @@ -285,7 +287,7 @@ Blockly.Xml.cloneShadow_ = function(shadow, opt_noId) { * Converts a DOM structure into plain text. * Currently the text format is fairly ugly: all one line with no whitespace, * unless the DOM itself has whitespace built-in. - * @param {!Element} dom A tree of XML elements. + * @param {!Node} dom A tree of XML nodes. * @return {string} Text representation. */ Blockly.Xml.domToText = function(dom) { @@ -306,7 +308,7 @@ Blockly.Xml.domToText = function(dom) { /** * Converts a DOM structure into properly indented text. - * @param {!Element} dom A tree of XML elements. + * @param {!Node} dom A tree of XML elements. * @return {string} Text representation. */ Blockly.Xml.domToPrettyText = function(dom) { @@ -371,6 +373,8 @@ Blockly.Xml.clearWorkspaceAndLoadFromXml = function(xml, workspace) { * @param {!Element} xml XML DOM. * @param {!Blockly.Workspace} workspace The workspace. * @return {!Array.} An array containing new block IDs. + * @suppress {strictModuleDepCheck} Suppress module check while workspace + * comments are not bundled in. */ Blockly.Xml.domToWorkspace = function(xml, workspace) { if (xml instanceof Blockly.Workspace) { @@ -389,10 +393,6 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) { } var newBlockIds = []; // A list of block IDs added by this call. Blockly.utils.dom.startTextWidthCache(); - // Safari 7.1.3 is known to provide node lists with extra references to - // children beyond the lists' length. Trust the length, do not use the - // looping pattern of checking the index for an object. - var childCount = xml.childNodes.length; var existingGroup = Blockly.Events.getGroup(); if (!existingGroup) { Blockly.Events.setGroup(true); @@ -404,20 +404,20 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) { } var variablesFirst = true; try { - for (var i = 0; i < childCount; i++) { - var xmlChild = xml.childNodes[i]; + for (var i = 0, xmlChild; (xmlChild = xml.childNodes[i]); i++) { var name = xmlChild.nodeName.toLowerCase(); + var xmlChildElement = /** @type {!Element} */ (xmlChild); if (name == 'block' || (name == 'shadow' && !Blockly.Events.recordUndo)) { // Allow top-level shadow blocks if recordUndo is disabled since // that means an undo is in progress. Such a block is expected // to be moved to a nested destination in the next operation. - var block = Blockly.Xml.domToBlock(xmlChild, workspace); + var block = Blockly.Xml.domToBlock(xmlChildElement, workspace); newBlockIds.push(block.id); - var blockX = xmlChild.hasAttribute('x') ? - parseInt(xmlChild.getAttribute('x'), 10) : 10; - var blockY = xmlChild.hasAttribute('y') ? - parseInt(xmlChild.getAttribute('y'), 10) : 10; + var blockX = xmlChildElement.hasAttribute('x') ? + parseInt(xmlChildElement.getAttribute('x'), 10) : 10; + var blockY = xmlChildElement.hasAttribute('y') ? + parseInt(xmlChildElement.getAttribute('y'), 10) : 10; if (!isNaN(blockX) && !isNaN(blockY)) { block.moveBy(workspace.RTL ? width - blockX : blockX, blockY); } @@ -430,19 +430,20 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) { console.warn('Missing require for Blockly.WorkspaceCommentSvg, ' + 'ignoring workspace comment.'); } else { - Blockly.WorkspaceCommentSvg.fromXml(xmlChild, workspace, width); + Blockly.WorkspaceCommentSvg.fromXml( + xmlChildElement, workspace, width); } } else { if (!Blockly.WorkspaceComment) { console.warn('Missing require for Blockly.WorkspaceComment, ' + 'ignoring workspace comment.'); } else { - Blockly.WorkspaceComment.fromXml(xmlChild, workspace); + Blockly.WorkspaceComment.fromXml(xmlChildElement, workspace); } } } else if (name == 'variables') { if (variablesFirst) { - Blockly.Xml.domToVariables(xmlChild, workspace); + Blockly.Xml.domToVariables(xmlChildElement, workspace); } else { throw Error('\'variables\' tag must exist once before block and ' + 'shadow tag elements in the workspace XML, but it was found in ' + @@ -477,44 +478,38 @@ Blockly.Xml.appendDomToWorkspace = function(xml, workspace) { // First check if we have a workspaceSvg, otherwise the blocks have no shape // and the position does not matter. if (workspace.hasOwnProperty('scale')) { - var savetab = Blockly.BlockSvg.TAB_WIDTH; - try { - Blockly.BlockSvg.TAB_WIDTH = 0; - bbox = workspace.getBlocksBoundingBox(); - } finally { - Blockly.BlockSvg.TAB_WIDTH = savetab; - } + bbox = workspace.getBlocksBoundingBox(); } // Load the new blocks into the workspace and get the IDs of the new blocks. - var newBlockIds = Blockly.Xml.domToWorkspace(xml,workspace); + var newBlockIds = Blockly.Xml.domToWorkspace(xml, workspace); if (bbox && bbox.top != bbox.bottom) { // check if any previous block var offsetY = 0; // offset to add to y of the new block var offsetX = 0; var farY = bbox.bottom; // bottom position - var topX = bbox.left; // x of bounding box + var topX = workspace.RTL ? bbox.right : bbox.left; // x of bounding box // Check position of the new blocks. - var newX = Infinity; // x of top corner + var newLeftX = Infinity; // x of top left corner + var newRightX = -Infinity; // x of top right corner var newY = Infinity; // y of top corner + var ySeparation = 10; for (var i = 0; i < newBlockIds.length; i++) { var blockXY = workspace.getBlockById(newBlockIds[i]).getRelativeToSurfaceXY(); if (blockXY.y < newY) { newY = blockXY.y; } - if (blockXY.x < newX) { // if we align also on x - newX = blockXY.x; + if (blockXY.x < newLeftX) { // if we left align also on x + newLeftX = blockXY.x; + } + if (blockXY.x > newRightX) { // if we right align also on x + newRightX = blockXY.x; } } - offsetY = farY - newY + Blockly.BlockSvg.SEP_SPACE_Y; - offsetX = topX - newX; - // move the new blocks to append them at the bottom - var width; // Not used in LTR. - if (workspace.RTL) { - width = workspace.getWidth(); - } + offsetY = farY - newY + ySeparation; + offsetX = workspace.RTL ? topX - newRightX : topX - newLeftX; for (var i = 0; i < newBlockIds.length; i++) { var block = workspace.getBlockById(newBlockIds[i]); - block.moveBy(workspace.RTL ? width - offsetX : offsetX, offsetY); + block.moveBy(offsetX, offsetY); } } return newBlockIds; @@ -545,8 +540,8 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { // Generate list of all blocks. var blocks = topBlock.getDescendants(false); if (workspace.rendered) { - // Hide connections to speed up assembly. - topBlock.setConnectionsHidden(true); + // Wait to track connections to speed up assembly. + topBlock.setConnectionTracking(false); // Render each block. for (var i = blocks.length - 1; i >= 0; i--) { blocks[i].initSvg(); @@ -557,8 +552,8 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { // Populating the connection database may be deferred until after the // blocks have rendered. setTimeout(function() { - if (topBlock.workspace) { // Check that the block hasn't been deleted. - topBlock.setConnectionsHidden(false); + if (!topBlock.disposed) { + topBlock.setConnectionTracking(true); } }, 1); topBlock.updateDisabled(); @@ -596,7 +591,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { * should be added. */ Blockly.Xml.domToVariables = function(xmlVariables, workspace) { - for (var i = 0, xmlChild; xmlChild = xmlVariables.childNodes[i]; i++) { + for (var i = 0, xmlChild; (xmlChild = xmlVariables.childNodes[i]); i++) { if (xmlChild.nodeType != Blockly.utils.dom.Node.ELEMENT_NODE) { continue; // Skip text nodes. } @@ -626,7 +621,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { block = workspace.newBlock(prototypeName, id); var blockChild = null; - for (var i = 0, xmlChild; xmlChild = xmlBlock.childNodes[i]; i++) { + for (var i = 0, xmlChild; (xmlChild = xmlBlock.childNodes[i]); i++) { if (xmlChild.nodeType == Blockly.utils.dom.Node.TEXT_NODE) { // Ignore any text at the level. It's all whitespace anyway. continue; @@ -636,7 +631,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { // Find any enclosed blocks or shadows in this tag. var childBlockElement = null; var childShadowElement = null; - for (var j = 0, grandchild; grandchild = xmlChild.childNodes[j]; j++) { + for (var j = 0, grandchild; (grandchild = xmlChild.childNodes[j]); j++) { if (grandchild.nodeType == Blockly.utils.dom.Node.ELEMENT_NODE) { if (grandchild.nodeName.toLowerCase() == 'block') { childBlockElement = /** @type {!Element} */ (grandchild); @@ -651,11 +646,12 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { } var name = xmlChild.getAttribute('name'); + var xmlChildElement = /** @type {!Element} */ (xmlChild); switch (xmlChild.nodeName.toLowerCase()) { case 'mutation': // Custom data for an advanced block. if (block.domToMutation) { - block.domToMutation(xmlChild); + block.domToMutation(xmlChildElement); if (block.initSvg) { // Mutation may have added some elements that need initializing. block.initSvg(); @@ -668,10 +664,10 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { 'ignoring block comment.'); break; } - var text = xmlChild.textContent; - var pinned = xmlChild.getAttribute('pinned') == 'true'; - var width = parseInt(xmlChild.getAttribute('w'), 10); - var height = parseInt(xmlChild.getAttribute('h'), 10); + var text = xmlChildElement.textContent; + var pinned = xmlChildElement.getAttribute('pinned') == 'true'; + var width = parseInt(xmlChildElement.getAttribute('w'), 10); + var height = parseInt(xmlChildElement.getAttribute('h'), 10); block.setCommentText(text); block.commentModel.pinned = pinned; @@ -692,7 +688,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { // Titles were renamed to field in December 2013. // Fall through. case 'field': - Blockly.Xml.domToField_(block, name, xmlChild); + Blockly.Xml.domToField_(block, name, xmlChildElement); break; case 'value': case 'statement': @@ -771,7 +767,7 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { if (xmlBlock.nodeName.toLowerCase() == 'shadow') { // Ensure all children are also shadows. var children = block.getChildren(false); - for (var i = 0, child; child = children[i]; i++) { + for (var i = 0, child; (child = children[i]); i++) { if (!child.isShadow()) { throw TypeError('Shadow block not allowed non-shadow child.'); } @@ -807,7 +803,7 @@ Blockly.Xml.domToField_ = function(block, fieldName, xml) { * @param {!Element} xmlBlock XML block element. */ Blockly.Xml.deleteNext = function(xmlBlock) { - for (var i = 0, child; child = xmlBlock.childNodes[i]; i++) { + for (var i = 0, child; (child = xmlBlock.childNodes[i]); i++) { if (child.nodeName.toLowerCase() == 'next') { xmlBlock.removeChild(child); break; diff --git a/dart_compressed.js b/dart_compressed.js index 9168aeac9..264e3de5d 100644 --- a/dart_compressed.js +++ b/dart_compressed.js @@ -1,4 +1,4 @@ -// Do not edit this file; automatically generated by build.py. +// Do not edit this file; automatically generated by gulp. 'use strict'; @@ -6,7 +6,7 @@ Blockly.Dart=new Blockly.Generator("Dart");Blockly.Dart.addReservedWords("assert 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",GTE:">="}[a.getFieldValue("OP")],c="=="==b||"!="==b?Blockly.Dart.ORDER_EQUALITY:Blockly.Dart.ORDER_RELATIONAL,d=Blockly.Dart.valueToCode(a,"A",c)||"0";a=Blockly.Dart.valueToCode(a,"B",c)||"0";return[d+" "+b+" "+a,c]}; Blockly.Dart.logic_operation=function(a){var b="AND"==a.getFieldValue("OP")?"&&":"||",c="&&"==b?Blockly.Dart.ORDER_LOGICAL_AND:Blockly.Dart.ORDER_LOGICAL_OR,d=Blockly.Dart.valueToCode(a,"A",c);a=Blockly.Dart.valueToCode(a,"B",c);if(d||a){var e="&&"==b?"true":"false";d||(d=e);a||(a=e)}else a=d="false";return[d+" "+b+" "+a,c]};Blockly.Dart.logic_negate=function(a){var b=Blockly.Dart.ORDER_UNARY_PREFIX;return["!"+(Blockly.Dart.valueToCode(a,"BOOL",b)||"true"),b]}; Blockly.Dart.logic_boolean=function(a){return["TRUE"==a.getFieldValue("BOOL")?"true":"false",Blockly.Dart.ORDER_ATOMIC]};Blockly.Dart.logic_null=function(a){return["null",Blockly.Dart.ORDER_ATOMIC]};Blockly.Dart.logic_ternary=function(a){var b=Blockly.Dart.valueToCode(a,"IF",Blockly.Dart.ORDER_CONDITIONAL)||"false",c=Blockly.Dart.valueToCode(a,"THEN",Blockly.Dart.ORDER_CONDITIONAL)||"null";a=Blockly.Dart.valueToCode(a,"ELSE",Blockly.Dart.ORDER_CONDITIONAL)||"null";return[b+" ? "+c+" : "+a,Blockly.Dart.ORDER_CONDITIONAL]};Blockly.Dart.loops={}; -Blockly.Dart.controls_repeat_ext=function(a){var b=a.getField("TIMES")?String(Number(a.getFieldValue("TIMES"))):Blockly.Dart.valueToCode(a,"TIMES",Blockly.Dart.ORDER_ASSIGNMENT)||"0",c=Blockly.Dart.statementToCode(a,"DO");c=Blockly.Dart.addLoopTrap(c,a);a="";var d=Blockly.Dart.variableDB_.getDistinctName("count",Blockly.Variables.NAME_TYPE),e=b;b.match(/^\w+$/)||Blockly.isNumber(b)||(e=Blockly.Dart.variableDB_.getDistinctName("repeat_end",Blockly.Variables.NAME_TYPE),a+="var "+e+" = "+b+";\n");return a+ -("for (int "+d+" = 0; "+d+" < "+e+"; "+d+"++) {\n"+c+"}\n")};Blockly.Dart.controls_repeat=Blockly.Dart.controls_repeat_ext;Blockly.Dart.controls_whileUntil=function(a){var b="UNTIL"==a.getFieldValue("MODE"),c=Blockly.Dart.valueToCode(a,"BOOL",b?Blockly.Dart.ORDER_UNARY_PREFIX:Blockly.Dart.ORDER_NONE)||"false",d=Blockly.Dart.statementToCode(a,"DO");d=Blockly.Dart.addLoopTrap(d,a);b&&(c="!"+c);return"while ("+c+") {\n"+d+"}\n"}; -Blockly.Dart.controls_for=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE),c=Blockly.Dart.valueToCode(a,"FROM",Blockly.Dart.ORDER_ASSIGNMENT)||"0",d=Blockly.Dart.valueToCode(a,"TO",Blockly.Dart.ORDER_ASSIGNMENT)||"0",e=Blockly.Dart.valueToCode(a,"BY",Blockly.Dart.ORDER_ASSIGNMENT)||"1",f=Blockly.Dart.statementToCode(a,"DO");f=Blockly.Dart.addLoopTrap(f,a);if(Blockly.isNumber(c)&&Blockly.isNumber(d)&&Blockly.isNumber(e)){var g=Number(c)<=Number(d); -a="for ("+b+" = "+c+"; "+b+(g?" <= ":" >= ")+d+"; "+b;b=Math.abs(Number(e));a=(1==b?a+(g?"++":"--"):a+((g?" += ":" -= ")+b))+(") {\n"+f+"}\n")}else a="",g=c,c.match(/^\w+$/)||Blockly.isNumber(c)||(g=Blockly.Dart.variableDB_.getDistinctName(b+"_start",Blockly.Variables.NAME_TYPE),a+="var "+g+" = "+c+";\n"),c=d,d.match(/^\w+$/)||Blockly.isNumber(d)||(c=Blockly.Dart.variableDB_.getDistinctName(b+"_end",Blockly.Variables.NAME_TYPE),a+="var "+c+" = "+d+";\n"),d=Blockly.Dart.variableDB_.getDistinctName(b+ -"_inc",Blockly.Variables.NAME_TYPE),a+="num "+d+" = ",a=Blockly.isNumber(e)?a+(Math.abs(e)+";\n"):a+("("+e+").abs();\n"),a=a+("if ("+g+" > "+c+") {\n")+(Blockly.Dart.INDENT+d+" = -"+d+";\n"),a+="}\n",a+="for ("+b+" = "+g+"; "+d+" >= 0 ? "+b+" <= "+c+" : "+b+" >= "+c+"; "+b+" += "+d+") {\n"+f+"}\n";return a}; -Blockly.Dart.controls_forEach=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE),c=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_ASSIGNMENT)||"[]",d=Blockly.Dart.statementToCode(a,"DO");d=Blockly.Dart.addLoopTrap(d,a);return"for (var "+b+" in "+c+") {\n"+d+"}\n"}; +Blockly.Dart.controls_repeat_ext=function(a){var b=a.getField("TIMES")?String(Number(a.getFieldValue("TIMES"))):Blockly.Dart.valueToCode(a,"TIMES",Blockly.Dart.ORDER_ASSIGNMENT)||"0",c=Blockly.Dart.statementToCode(a,"DO");c=Blockly.Dart.addLoopTrap(c,a);a="";var d=Blockly.Dart.variableDB_.getDistinctName("count",Blockly.VARIABLE_CATEGORY_NAME),e=b;b.match(/^\w+$/)||Blockly.isNumber(b)||(e=Blockly.Dart.variableDB_.getDistinctName("repeat_end",Blockly.VARIABLE_CATEGORY_NAME),a+="var "+e+" = "+b+";\n"); +return a+("for (int "+d+" = 0; "+d+" < "+e+"; "+d+"++) {\n"+c+"}\n")};Blockly.Dart.controls_repeat=Blockly.Dart.controls_repeat_ext;Blockly.Dart.controls_whileUntil=function(a){var b="UNTIL"==a.getFieldValue("MODE"),c=Blockly.Dart.valueToCode(a,"BOOL",b?Blockly.Dart.ORDER_UNARY_PREFIX:Blockly.Dart.ORDER_NONE)||"false",d=Blockly.Dart.statementToCode(a,"DO");d=Blockly.Dart.addLoopTrap(d,a);b&&(c="!"+c);return"while ("+c+") {\n"+d+"}\n"}; +Blockly.Dart.controls_for=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.VARIABLE_CATEGORY_NAME),c=Blockly.Dart.valueToCode(a,"FROM",Blockly.Dart.ORDER_ASSIGNMENT)||"0",d=Blockly.Dart.valueToCode(a,"TO",Blockly.Dart.ORDER_ASSIGNMENT)||"0",e=Blockly.Dart.valueToCode(a,"BY",Blockly.Dart.ORDER_ASSIGNMENT)||"1",f=Blockly.Dart.statementToCode(a,"DO");f=Blockly.Dart.addLoopTrap(f,a);if(Blockly.isNumber(c)&&Blockly.isNumber(d)&&Blockly.isNumber(e)){var g=Number(c)<=Number(d); +a="for ("+b+" = "+c+"; "+b+(g?" <= ":" >= ")+d+"; "+b;b=Math.abs(Number(e));a=(1==b?a+(g?"++":"--"):a+((g?" += ":" -= ")+b))+(") {\n"+f+"}\n")}else a="",g=c,c.match(/^\w+$/)||Blockly.isNumber(c)||(g=Blockly.Dart.variableDB_.getDistinctName(b+"_start",Blockly.VARIABLE_CATEGORY_NAME),a+="var "+g+" = "+c+";\n"),c=d,d.match(/^\w+$/)||Blockly.isNumber(d)||(c=Blockly.Dart.variableDB_.getDistinctName(b+"_end",Blockly.VARIABLE_CATEGORY_NAME),a+="var "+c+" = "+d+";\n"),d=Blockly.Dart.variableDB_.getDistinctName(b+ +"_inc",Blockly.VARIABLE_CATEGORY_NAME),a+="num "+d+" = ",a=Blockly.isNumber(e)?a+(Math.abs(e)+";\n"):a+("("+e+").abs();\n"),a=a+("if ("+g+" > "+c+") {\n")+(Blockly.Dart.INDENT+d+" = -"+d+";\n"),a+="}\n",a+="for ("+b+" = "+g+"; "+d+" >= 0 ? "+b+" <= "+c+" : "+b+" >= "+c+"; "+b+" += "+d+") {\n"+f+"}\n";return a}; +Blockly.Dart.controls_forEach=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.VARIABLE_CATEGORY_NAME),c=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_ASSIGNMENT)||"[]",d=Blockly.Dart.statementToCode(a,"DO");d=Blockly.Dart.addLoopTrap(d,a);return"for (var "+b+" in "+c+") {\n"+d+"}\n"}; Blockly.Dart.controls_flow_statements=function(a){var b="";Blockly.Dart.STATEMENT_PREFIX&&(b+=Blockly.Dart.injectId(Blockly.Dart.STATEMENT_PREFIX,a));Blockly.Dart.STATEMENT_SUFFIX&&(b+=Blockly.Dart.injectId(Blockly.Dart.STATEMENT_SUFFIX,a));if(Blockly.Dart.STATEMENT_PREFIX){var c=Blockly.Constants.Loops.CONTROL_FLOW_IN_LOOP_CHECK_MIXIN.getSurroundLoop(a);c&&!c.suppressPrefixSuffix&&(b+=Blockly.Dart.injectId(Blockly.Dart.STATEMENT_PREFIX,c))}switch(a.getFieldValue("FLOW")){case "BREAK":return b+"break;\n"; case "CONTINUE":return b+"continue;\n"}throw Error("Unknown flow statement.");};Blockly.Dart.math={};Blockly.Dart.addReservedWords("Math");Blockly.Dart.math_number=function(a){a=Number(a.getFieldValue("NUM"));if(Infinity==a){a="double.infinity";var b=Blockly.Dart.ORDER_UNARY_POSTFIX}else-Infinity==a?(a="-double.infinity",b=Blockly.Dart.ORDER_UNARY_PREFIX):b=0>a?Blockly.Dart.ORDER_UNARY_PREFIX:Blockly.Dart.ORDER_ATOMIC;return[a,b]}; Blockly.Dart.math_arithmetic=function(a){var b={ADD:[" + ",Blockly.Dart.ORDER_ADDITIVE],MINUS:[" - ",Blockly.Dart.ORDER_ADDITIVE],MULTIPLY:[" * ",Blockly.Dart.ORDER_MULTIPLICATIVE],DIVIDE:[" / ",Blockly.Dart.ORDER_MULTIPLICATIVE],POWER:[null,Blockly.Dart.ORDER_NONE]}[a.getFieldValue("OP")],c=b[0];b=b[1];var d=Blockly.Dart.valueToCode(a,"A",b)||"0";a=Blockly.Dart.valueToCode(a,"B",b)||"0";return c?[d+c+a,b]:(Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;",["Math.pow("+d+", "+ @@ -63,7 +63,7 @@ return b[a]}; Blockly.Dart.math_number_property=function(a){var b=Blockly.Dart.valueToCode(a,"NUMBER_TO_CHECK",Blockly.Dart.ORDER_MULTIPLICATIVE);if(!b)return["false",Blockly.Dart.ORDER_ATOMIC];var c=a.getFieldValue("PROPERTY");if("PRIME"==c)return Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;",[Blockly.Dart.provideFunction_("math_isPrime",["bool "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(n) {"," // https://en.wikipedia.org/wiki/Primality_test#Naive_methods"," if (n == 2 || n == 3) {"," return true;", " }"," // False if n is null, negative, is 1, or not whole."," // And false if n is divisible by 2 or 3."," if (n == null || n <= 1 || n % 1 != 0 || n % 2 == 0 || n % 3 == 0) {"," return false;"," }"," // Check all the numbers of form 6k +/- 1, up to sqrt(n)."," for (var x = 6; x <= Math.sqrt(n) + 1; x += 6) {"," if (n % (x - 1) == 0 || n % (x + 1) == 0) {"," return false;"," }"," }"," return true;","}"])+"("+b+")",Blockly.Dart.ORDER_UNARY_POSTFIX];switch(c){case "EVEN":var d= b+" % 2 == 0";break;case "ODD":d=b+" % 2 == 1";break;case "WHOLE":d=b+" % 1 == 0";break;case "POSITIVE":d=b+" > 0";break;case "NEGATIVE":d=b+" < 0";break;case "DIVISIBLE_BY":a=Blockly.Dart.valueToCode(a,"DIVISOR",Blockly.Dart.ORDER_MULTIPLICATIVE);if(!a)return["false",Blockly.Dart.ORDER_ATOMIC];d=b+" % "+a+" == 0"}return[d,Blockly.Dart.ORDER_EQUALITY]}; -Blockly.Dart.math_change=function(a){var b=Blockly.Dart.valueToCode(a,"DELTA",Blockly.Dart.ORDER_ADDITIVE)||"0";a=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.Variables.NAME_TYPE);return a+" = ("+a+" is num ? "+a+" : 0) + "+b+";\n"};Blockly.Dart.math_round=Blockly.Dart.math_single;Blockly.Dart.math_trig=Blockly.Dart.math_single; +Blockly.Dart.math_change=function(a){var b=Blockly.Dart.valueToCode(a,"DELTA",Blockly.Dart.ORDER_ADDITIVE)||"0";a=Blockly.Dart.variableDB_.getName(a.getFieldValue("VAR"),Blockly.VARIABLE_CATEGORY_NAME);return a+" = ("+a+" is num ? "+a+" : 0) + "+b+";\n"};Blockly.Dart.math_round=Blockly.Dart.math_single;Blockly.Dart.math_trig=Blockly.Dart.math_single; Blockly.Dart.math_on_list=function(a){var b=a.getFieldValue("OP");a=Blockly.Dart.valueToCode(a,"LIST",Blockly.Dart.ORDER_NONE)||"[]";switch(b){case "SUM":b=Blockly.Dart.provideFunction_("math_sum",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," num sumVal = 0;"," myList.forEach((num entry) {sumVal += entry;});"," return sumVal;","}"]);b=b+"("+a+")";break;case "MIN":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_min", ["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," if (myList.isEmpty) return null;"," num minVal = myList[0];"," myList.forEach((num entry) {minVal = Math.min(minVal, entry);});"," return minVal;","}"]);b=b+"("+a+")";break;case "MAX":Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";b=Blockly.Dart.provideFunction_("math_max",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," if (myList.isEmpty) return null;"," num maxVal = myList[0];", " myList.forEach((num entry) {maxVal = Math.max(maxVal, entry);});"," return maxVal;","}"]);b=b+"("+a+")";break;case "AVERAGE":b=Blockly.Dart.provideFunction_("math_mean",["num "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(List myList) {"," // First filter list for numbers only."," List localList = new List.from(myList);"," localList.removeWhere((a) => a is! num);"," if (localList.isEmpty) return null;"," num sumVal = 0;"," localList.forEach((var entry) {sumVal += entry;});"," return sumVal / localList.length;", @@ -77,12 +77,12 @@ Blockly.Dart.math_constrain=function(a){Blockly.Dart.definitions_.import_dart_ma Blockly.Dart.math_random_int=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";var b=Blockly.Dart.valueToCode(a,"FROM",Blockly.Dart.ORDER_NONE)||"0";a=Blockly.Dart.valueToCode(a,"TO",Blockly.Dart.ORDER_NONE)||"0";return[Blockly.Dart.provideFunction_("math_random_int",["int "+Blockly.Dart.FUNCTION_NAME_PLACEHOLDER_+"(num a, num b) {"," if (a > b) {"," // Swap a and b to ensure a is smaller."," num c = a;"," a = b;"," b = c;"," }"," return new Math.Random().nextInt(b - a + 1) + a;", "}"])+"("+b+", "+a+")",Blockly.Dart.ORDER_UNARY_POSTFIX]};Blockly.Dart.math_random_float=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";return["new Math.Random().nextDouble()",Blockly.Dart.ORDER_UNARY_POSTFIX]}; Blockly.Dart.math_atan2=function(a){Blockly.Dart.definitions_.import_dart_math="import 'dart:math' as Math;";var b=Blockly.Dart.valueToCode(a,"X",Blockly.Dart.ORDER_NONE)||"0";return["Math.atan2("+(Blockly.Dart.valueToCode(a,"Y",Blockly.Dart.ORDER_NONE)||"0")+", "+b+") / Math.pi * 180",Blockly.Dart.ORDER_MULTIPLICATIVE]};Blockly.Dart.procedures={}; -Blockly.Dart.procedures_defreturn=function(a){var b=Blockly.Dart.variableDB_.getName(a.getFieldValue("NAME"),Blockly.Procedures.NAME_TYPE),c="";Blockly.Dart.STATEMENT_PREFIX&&(c+=Blockly.Dart.injectId(Blockly.Dart.STATEMENT_PREFIX,a));Blockly.Dart.STATEMENT_SUFFIX&&(c+=Blockly.Dart.injectId(Blockly.Dart.STATEMENT_SUFFIX,a));c&&(c=Blockly.Dart.prefixLines(c,Blockly.Dart.INDENT));var d="";Blockly.Dart.INFINITE_LOOP_TRAP&&(d=Blockly.Dart.prefixLines(Blockly.Dart.injectId(Blockly.Dart.INFINITE_LOOP_TRAP,a), -Blockly.Dart.INDENT));var e=Blockly.Dart.statementToCode(a,"STACK"),f=Blockly.Dart.valueToCode(a,"RETURN",Blockly.Dart.ORDER_NONE)||"",g="";e&&f&&(g=c);f&&(f=Blockly.Dart.INDENT+"return "+f+";\n");for(var l=f?"dynamic":"void",k=[],h=0;h} libBlockTypes Array of block types from the block @@ -1323,7 +1323,7 @@ WorkspaceFactoryController.prototype.warnForUndefinedBlocks_ = function() { } }; -/* +/** * Determines if a standard variable category is in the custom toolbox. * @return {boolean} True if a variables category is in use, false otherwise. */ diff --git a/demos/blockfactory/workspacefactory/wfactory_model.js b/demos/blockfactory/workspacefactory/wfactory_model.js index a295a496a..ff9eaff8e 100644 --- a/demos/blockfactory/workspacefactory/wfactory_model.js +++ b/demos/blockfactory/workspacefactory/wfactory_model.js @@ -390,7 +390,7 @@ WorkspaceFactoryModel.prototype.setOptions = function(options) { this.options = options; }; -/* +/** * Returns an array of all the block types currently being used in the toolbox * and the pre-loaded blocks. No duplicates. * TODO(evd2014): Move pushBlockTypesToList to FactoryUtils. diff --git a/demos/code/code.js b/demos/code/code.js index 67cace73c..e19b9f532 100644 --- a/demos/code/code.js +++ b/demos/code/code.js @@ -179,6 +179,15 @@ Code.changeLanguage = function() { window.location.host + window.location.pathname + search; }; +/** + * Changes the output language by clicking the tab matching + * the selected language in the codeMenu. + */ +Code.changeCodingLanguage = function() { + var codeMenu = document.getElementById('code_menu'); + Code.tabClick(codeMenu.options[codeMenu.selectedIndex].value); +} + /** * Bind a function to a button's click event. * On touch enabled browsers, ontouchend is treated as equivalent to onclick. @@ -238,6 +247,14 @@ Code.LANG = Code.getLang(); */ Code.TABS_ = ['blocks', 'javascript', 'php', 'python', 'dart', 'lua', 'xml']; +/** + * List of tab names with casing, for display in the UI. + * @private + */ +Code.TABS_DISPLAY_ = [ + 'Blocks', 'JavaScript', 'PHP', 'Python', 'Dart', 'Lua', 'XML', +]; + Code.selected = 'blocks'; /** @@ -246,7 +263,7 @@ Code.selected = 'blocks'; */ Code.tabClick = function(clickedName) { // If the XML tab was open, save and render the content. - if (document.getElementById('tab_xml').className == 'tabon') { + if (document.getElementById('tab_xml').classList.contains('tabon')) { var xmlTextarea = document.getElementById('content_xml'); var xmlText = xmlTextarea.value; var xmlDom = null; @@ -266,25 +283,42 @@ Code.tabClick = function(clickedName) { } } - if (document.getElementById('tab_blocks').className == 'tabon') { + if (document.getElementById('tab_blocks').classList.contains('tabon')) { Code.workspace.setVisible(false); } // Deselect all tabs and hide all panes. for (var i = 0; i < Code.TABS_.length; i++) { var name = Code.TABS_[i]; - document.getElementById('tab_' + name).className = 'taboff'; + var tab = document.getElementById('tab_' + name); + tab.classList.add('taboff'); + tab.classList.remove('tabon'); document.getElementById('content_' + name).style.visibility = 'hidden'; } // Select the active tab. Code.selected = clickedName; - document.getElementById('tab_' + clickedName).className = 'tabon'; + var selectedTab = document.getElementById('tab_' + clickedName); + selectedTab.classList.remove('taboff'); + selectedTab.classList.add('tabon'); // Show the selected pane. document.getElementById('content_' + clickedName).style.visibility = 'visible'; Code.renderContent(); + // The code menu tab is on if the blocks tab is off. + var codeMenuTab = document.getElementById('tab_code'); if (clickedName == 'blocks') { Code.workspace.setVisible(true); + codeMenuTab.className = 'taboff'; + } else { + codeMenuTab.className = 'tabon'; + } + // Sync the menu's value with the clicked tab value if needed. + var codeMenu = document.getElementById('code_menu'); + for (var i = 0; i < codeMenu.options.length; i++) { + if (codeMenu.options[i].value == clickedName) { + codeMenu.selectedIndex = i; + break; + } } Blockly.svgResize(Code.workspace); }; @@ -379,9 +413,9 @@ Code.init = function() { el.style.width = (2 * bBox.width - el.offsetWidth) + 'px'; } // Make the 'Blocks' tab line up with the toolbox. - if (Code.workspace && Code.workspace.toolbox_.width) { + if (Code.workspace && Code.workspace.getToolbox().width) { document.getElementById('tab_blocks').style.minWidth = - (Code.workspace.toolbox_.width - 38) + 'px'; + (Code.workspace.getToolbox().width - 38) + 'px'; // Account for the 19 pixel margin and on each side. } }; @@ -455,6 +489,14 @@ Code.init = function() { Code.bindClick('tab_' + name, function(name_) {return function() {Code.tabClick(name_);};}(name)); } + Code.bindClick('tab_code', function(e) { + if (e.target !== document.getElementById('tab_code')) { + // Prevent clicks on child codeMenu from triggering a tab click. + return; + } + Code.changeCodingLanguage(); + }); + onresize(); Blockly.svgResize(Code.workspace); @@ -497,6 +539,14 @@ Code.initLanguage = function() { } languageMenu.addEventListener('change', Code.changeLanguage, true); + // Populate the coding language selection menu. + var codeMenu = document.getElementById('code_menu'); + codeMenu.options.length = 0; + for (var i = 1; i < Code.TABS_.length; i++) { + codeMenu.options.add(new Option(Code.TABS_DISPLAY_[i], Code.TABS_[i])); + } + codeMenu.addEventListener('change', Code.changeCodingLanguage); + // Inject language strings. document.title += ' ' + MSG['title']; document.getElementById('title').textContent = MSG['title']; diff --git a/demos/code/index.html b/demos/code/index.html index fdf083e95..298613f58 100644 --- a/demos/code/index.html +++ b/demos/code/index.html @@ -2,6 +2,7 @@ + Blockly Demo: @@ -33,18 +34,22 @@ + + + + + + + + + + + + - - - - - - - - - - - +
... JavaScript Python PHP Lua Dart XML  JavaScript Python PHP Lua Dart XML + +