diff --git a/blockly_compressed.js b/blockly_compressed.js index 38803b300..df8abc67c 100644 --- a/blockly_compressed.js +++ b/blockly_compressed.js @@ -13,7 +13,7 @@ }(this, function() { var $={}; var longStart$$module$build$src$core$touch=function(a,b){longStop$$module$build$src$core$touch();a.changedTouches&&1!==a.changedTouches.length||(longPid_$$module$build$src$core$touch=setTimeout(function(){a.changedTouches&&(a.button=2,a.clientX=a.changedTouches[0].clientX,a.clientY=a.changedTouches[0].clientY);b&&b.handleRightClick(a)},LONGPRESS$$module$build$src$core$touch))},longStop$$module$build$src$core$touch=function(){longPid_$$module$build$src$core$touch&&(clearTimeout(longPid_$$module$build$src$core$touch), -longPid_$$module$build$src$core$touch=0)},clearTouchIdentifier$$module$build$src$core$touch=function(){touchIdentifier_$$module$build$src$core$touch=null},shouldHandleEvent$$module$build$src$core$touch=function(a){return!isMouseOrTouchEvent$$module$build$src$core$touch(a)||checkTouchIdentifier$$module$build$src$core$touch(a)},getTouchIdentifierFromEvent$$module$build$src$core$touch=function(a){return a instanceof MouseEvent?"mouse":a instanceof PointerEvent?String(a.pointerId):a.changedTouches&&a.changedTouches[0]&& +longPid_$$module$build$src$core$touch=0)},clearTouchIdentifier$$module$build$src$core$touch=function(){touchIdentifier_$$module$build$src$core$touch=null},shouldHandleEvent$$module$build$src$core$touch=function(a){return!isMouseOrTouchEvent$$module$build$src$core$touch(a)||checkTouchIdentifier$$module$build$src$core$touch(a)},getTouchIdentifierFromEvent$$module$build$src$core$touch=function(a){return a instanceof PointerEvent?String(a.pointerId):a instanceof MouseEvent?"mouse":a.changedTouches&&a.changedTouches[0]&& void 0!==a.changedTouches[0].identifier&&null!==a.changedTouches[0].identifier?String(a.changedTouches[0].identifier):"mouse"},checkTouchIdentifier$$module$build$src$core$touch=function(a){const b=getTouchIdentifierFromEvent$$module$build$src$core$touch(a);return void 0!==touchIdentifier_$$module$build$src$core$touch&&null!==touchIdentifier_$$module$build$src$core$touch?touchIdentifier_$$module$build$src$core$touch===b:"mousedown"===a.type||"touchstart"===a.type||"pointerdown"===a.type?(touchIdentifier_$$module$build$src$core$touch= b,!0):!1},setClientFromTouch$$module$build$src$core$touch=function(a){if(a.type.startsWith("touch")&&a.changedTouches){const b=a.changedTouches[0];a.clientX=b.clientX;a.clientY=b.clientY}},isMouseOrTouchEvent$$module$build$src$core$touch=function(a){return a.type.startsWith("touch")||a.type.startsWith("mouse")||a.type.startsWith("pointer")},isTouchEvent$$module$build$src$core$touch=function(a){return a.type.startsWith("touch")||a.type.startsWith("pointer")},splitEventByTouches$$module$build$src$core$touch= function(a){const b=[];if(a.changedTouches)for(let 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=createSvgElement$$module$build$src$core$utils$dom(Svg$$module$build$src$core$utils$svg.CIRCLE,{cx:d.x,cy:d.y,r:0,fill:"none", stroke:"#888","stroke-width":10},b.getParentSvg());connectionUiStep$$module$build$src$core$block_animations(a,new Date,c)}},connectionUiStep$$module$build$src$core$block_animations=function(a,b,c){const d=((new Date).getTime()-b.getTime())/150;1a.workspace.scale)){var b=a.getHeightWidth().height;b=Math.atan(10/b)/Math.PI*180;a.RTL||(b*=-1);disconnectGroup$$module$build$src$core$block_animations=a.getSvgRoot();disconnectUiStep$$module$build$src$core$block_animations(disconnectGroup$$module$build$src$core$block_animations,b,new Date)}},disconnectUiStep$$module$build$src$core$block_animations= -function(a,b,c){const d=((new Date).getTime()-c.getTime())/200;let e="";1>=d&&(e=`skewX(${Math.round(Math.sin(d*Math.PI*3)*(1-d)*b)})`,disconnectPid$$module$build$src$core$block_animations=setTimeout(disconnectUiStep$$module$build$src$core$block_animations,10,a,b,c));a.setAttribute("transform",e)},disconnectUiStop$$module$build$src$core$block_animations=function(){disconnectGroup$$module$build$src$core$block_animations&&(disconnectPid$$module$build$src$core$block_animations&&clearTimeout(disconnectPid$$module$build$src$core$block_animations), -disconnectGroup$$module$build$src$core$block_animations.setAttribute("transform",""),disconnectGroup$$module$build$src$core$block_animations=null)},copy$$module$build$src$core$clipboard=function(a){TEST_ONLY$$module$build$src$core$clipboard.copyInternal(a)},copyInternal$$module$build$src$core$clipboard=function(a){copyData$$module$build$src$core$clipboard=a.toCopyData()},paste$$module$build$src$core$clipboard=function(){if(!copyData$$module$build$src$core$clipboard)return null;let a=copyData$$module$build$src$core$clipboard.source; -a.isFlyout&&(a=a.targetWorkspace);return copyData$$module$build$src$core$clipboard.typeCounts&&a.isCapacityAvailable(copyData$$module$build$src$core$clipboard.typeCounts)?a.paste(copyData$$module$build$src$core$clipboard.saveInfo):null},duplicate$$module$build$src$core$clipboard=function(a){return TEST_ONLY$$module$build$src$core$clipboard.duplicateInternal(a)},duplicateInternal$$module$build$src$core$clipboard=function(a){const b=copyData$$module$build$src$core$clipboard;copy$$module$build$src$core$clipboard(a); -let c,d,e;a=null!=(e=null==(c=a.toCopyData())?void 0:null==(d=c.source)?void 0:d.paste(copyData$$module$build$src$core$clipboard.saveInfo))?e:null;copyData$$module$build$src$core$clipboard=b;return a},getCurrentBlock$$module$build$src$core$contextmenu=function(){return currentBlock$$module$build$src$core$contextmenu},setCurrentBlock$$module$build$src$core$contextmenu=function(a){currentBlock$$module$build$src$core$contextmenu=a},show$$module$build$src$core$contextmenu=function(a,b,c){show$$module$build$src$core$widgetdiv(dummyOwner$$module$build$src$core$contextmenu, -c,dispose$$module$build$src$core$contextmenu);if(b.length){var d=populate_$$module$build$src$core$contextmenu(b,c);menu_$$module$build$src$core$contextmenu=d;position_$$module$build$src$core$contextmenu(d,a,c);setTimeout(function(){d.focus()},1);currentBlock$$module$build$src$core$contextmenu=null}else hide$$module$build$src$core$contextmenu()},populate_$$module$build$src$core$contextmenu=function(a,b){const c=new Menu$$module$build$src$core$menu;c.setRole(Role$$module$build$src$core$utils$aria.MENU); -for(let d=0;d{disable$$module$build$src$core$events$utils();let c;try{c=domToBlock$$module$build$src$core$xml(b,a.workspace);const d=a.getRelativeToSurfaceXY();d.x=a.RTL?d.x-$.config$$module$build$src$core$config.snapRadius:d.x+$.config$$module$build$src$core$config.snapRadius;d.y+=2*$.config$$module$build$src$core$config.snapRadius; -c.moveBy(d.x,d.y)}finally{enable$$module$build$src$core$events$utils()}isEnabled$$module$build$src$core$events$utils()&&!c.isShadow()&&fire$$module$build$src$core$events$utils(new (get$$module$build$src$core$events$utils(CREATE$$module$build$src$core$events$utils))(c));c.select()}},commentDeleteOption$$module$build$src$core$contextmenu=function(a){return{text:Msg$$module$build$src$core$msg.REMOVE_COMMENT,enabled:!0,callback:function(){setGroup$$module$build$src$core$events$utils(!0);a.dispose();setGroup$$module$build$src$core$events$utils(!1)}}}, -commentDuplicateOption$$module$build$src$core$contextmenu=function(a){return{text:Msg$$module$build$src$core$msg.DUPLICATE_COMMENT,enabled:!0,callback:function(){duplicate$$module$build$src$core$clipboard(a)}}},workspaceCommentOption$$module$build$src$core$contextmenu=function(a,b){const c={enabled:!0};c.text=Msg$$module$build$src$core$msg.ADD_COMMENT;c.callback=function(){const d=new WorkspaceCommentSvg$$module$build$src$core$workspace_comment_svg(a,Msg$$module$build$src$core$msg.WORKSPACE_COMMENT_DEFAULT_TEXT, -WorkspaceCommentSvg$$module$build$src$core$workspace_comment_svg.DEFAULT_SIZE,WorkspaceCommentSvg$$module$build$src$core$workspace_comment_svg.DEFAULT_SIZE);var e=a.getInjectionDiv().getBoundingClientRect();e=new Coordinate$$module$build$src$core$utils$coordinate(b.clientX-e.left,b.clientY-e.top);const f=a.getOriginOffsetInPixels();e=Coordinate$$module$build$src$core$utils$coordinate.difference(e,f);e.scale(1/a.scale);d.moveBy(e.x,e.y);a.rendered&&(d.initSvg(),d.render(),d.select())};return c},getStartPositionRect$$module$build$src$core$positionable_helpers= -function(a,b,c,d,e,f){const g=f.scrollbar&&f.scrollbar.canScrollVertically();a.horizontal===horizontalPosition$$module$build$src$core$positionable_helpers.LEFT?(c=e.absoluteMetrics.left+c,g&&f.RTL&&(c+=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness)):(c=e.absoluteMetrics.left+e.viewMetrics.width-b.width-c,g&&!f.RTL&&(c-=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness));a.vertical===verticalPosition$$module$build$src$core$positionable_helpers.TOP?a=e.absoluteMetrics.top+ -d:(a=e.absoluteMetrics.top+e.viewMetrics.height-b.height-d,f.scrollbar&&f.scrollbar.canScrollHorizontally()&&(a-=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness));return new Rect$$module$build$src$core$utils$rect(a,a+b.height,c,c+b.width)},getCornerOppositeToolbox$$module$build$src$core$positionable_helpers=function(a,b){return{horizontal:b.toolboxMetrics.position===Position$$module$build$src$core$utils$toolbox.LEFT||a.horizontalLayout&&!a.RTL?horizontalPosition$$module$build$src$core$positionable_helpers.RIGHT: -horizontalPosition$$module$build$src$core$positionable_helpers.LEFT,vertical:b.toolboxMetrics.position===Position$$module$build$src$core$utils$toolbox.BOTTOM?verticalPosition$$module$build$src$core$positionable_helpers.TOP:verticalPosition$$module$build$src$core$positionable_helpers.BOTTOM}},bumpPositionRect$$module$build$src$core$positionable_helpers=function(a,b,c,d){const e=a.left,f=a.right-a.left,g=a.bottom-a.top;for(let h=0;h=d&&(e=`skewX(${Math.round(Math.sin(d*Math.PI*3)*(1-d)*b)})`,disconnectPid$$module$build$src$core$block_animations=setTimeout(disconnectUiStep$$module$build$src$core$block_animations,10,a,b,c));a.skew_=e;a.setAttribute("transform",a.translate_+a.skew_)},disconnectUiStop$$module$build$src$core$block_animations=function(){if(disconnectGroup$$module$build$src$core$block_animations){disconnectPid$$module$build$src$core$block_animations&& +clearTimeout(disconnectPid$$module$build$src$core$block_animations);const a=disconnectGroup$$module$build$src$core$block_animations;a.skew_="";a.setAttribute("transform",a.translate_);disconnectGroup$$module$build$src$core$block_animations=null}},copy$$module$build$src$core$clipboard=function(a){TEST_ONLY$$module$build$src$core$clipboard.copyInternal(a)},copyInternal$$module$build$src$core$clipboard=function(a){copyData$$module$build$src$core$clipboard=a.toCopyData()},paste$$module$build$src$core$clipboard= +function(){if(!copyData$$module$build$src$core$clipboard)return null;let a=copyData$$module$build$src$core$clipboard.source;a.isFlyout&&(a=a.targetWorkspace);return copyData$$module$build$src$core$clipboard.typeCounts&&a.isCapacityAvailable(copyData$$module$build$src$core$clipboard.typeCounts)?a.paste(copyData$$module$build$src$core$clipboard.saveInfo):null},duplicate$$module$build$src$core$clipboard=function(a){return TEST_ONLY$$module$build$src$core$clipboard.duplicateInternal(a)},duplicateInternal$$module$build$src$core$clipboard= +function(a){const b=copyData$$module$build$src$core$clipboard;copy$$module$build$src$core$clipboard(a);let c,d,e;a=null!=(e=null==(c=a.toCopyData())?void 0:null==(d=c.source)?void 0:d.paste(copyData$$module$build$src$core$clipboard.saveInfo))?e:null;copyData$$module$build$src$core$clipboard=b;return a},getCurrentBlock$$module$build$src$core$contextmenu=function(){return currentBlock$$module$build$src$core$contextmenu},setCurrentBlock$$module$build$src$core$contextmenu=function(a){currentBlock$$module$build$src$core$contextmenu= +a},show$$module$build$src$core$contextmenu=function(a,b,c){show$$module$build$src$core$widgetdiv(dummyOwner$$module$build$src$core$contextmenu,c,dispose$$module$build$src$core$contextmenu);if(b.length){var d=populate_$$module$build$src$core$contextmenu(b,c);menu_$$module$build$src$core$contextmenu=d;position_$$module$build$src$core$contextmenu(d,a,c);setTimeout(function(){d.focus()},1);currentBlock$$module$build$src$core$contextmenu=null}else hide$$module$build$src$core$contextmenu()},populate_$$module$build$src$core$contextmenu= +function(a,b){const c=new Menu$$module$build$src$core$menu;c.setRole(Role$$module$build$src$core$utils$aria.MENU);for(let d=0;d{disable$$module$build$src$core$events$utils();let c;try{c=domToBlock$$module$build$src$core$xml(b,a.workspace);const d=a.getRelativeToSurfaceXY();d.x=a.RTL?d.x-$.config$$module$build$src$core$config.snapRadius:d.x+$.config$$module$build$src$core$config.snapRadius; +d.y+=2*$.config$$module$build$src$core$config.snapRadius;c.moveBy(d.x,d.y)}finally{enable$$module$build$src$core$events$utils()}isEnabled$$module$build$src$core$events$utils()&&!c.isShadow()&&fire$$module$build$src$core$events$utils(new (get$$module$build$src$core$events$utils(CREATE$$module$build$src$core$events$utils))(c));c.select()}},commentDeleteOption$$module$build$src$core$contextmenu=function(a){return{text:Msg$$module$build$src$core$msg.REMOVE_COMMENT,enabled:!0,callback:function(){setGroup$$module$build$src$core$events$utils(!0); +a.dispose();setGroup$$module$build$src$core$events$utils(!1)}}},commentDuplicateOption$$module$build$src$core$contextmenu=function(a){return{text:Msg$$module$build$src$core$msg.DUPLICATE_COMMENT,enabled:!0,callback:function(){duplicate$$module$build$src$core$clipboard(a)}}},workspaceCommentOption$$module$build$src$core$contextmenu=function(a,b){const c={enabled:!0};c.text=Msg$$module$build$src$core$msg.ADD_COMMENT;c.callback=function(){const d=new WorkspaceCommentSvg$$module$build$src$core$workspace_comment_svg(a, +Msg$$module$build$src$core$msg.WORKSPACE_COMMENT_DEFAULT_TEXT,WorkspaceCommentSvg$$module$build$src$core$workspace_comment_svg.DEFAULT_SIZE,WorkspaceCommentSvg$$module$build$src$core$workspace_comment_svg.DEFAULT_SIZE);var e=a.getInjectionDiv().getBoundingClientRect();e=new Coordinate$$module$build$src$core$utils$coordinate(b.clientX-e.left,b.clientY-e.top);const f=a.getOriginOffsetInPixels();e=Coordinate$$module$build$src$core$utils$coordinate.difference(e,f);e.scale(1/a.scale);d.moveBy(e.x,e.y); +a.rendered&&(d.initSvg(),d.render(),d.select())};return c},getStartPositionRect$$module$build$src$core$positionable_helpers=function(a,b,c,d,e,f){const g=f.scrollbar&&f.scrollbar.canScrollVertically();a.horizontal===horizontalPosition$$module$build$src$core$positionable_helpers.LEFT?(c=e.absoluteMetrics.left+c,g&&f.RTL&&(c+=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness)):(c=e.absoluteMetrics.left+e.viewMetrics.width-b.width-c,g&&!f.RTL&&(c-=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness)); +a.vertical===verticalPosition$$module$build$src$core$positionable_helpers.TOP?a=e.absoluteMetrics.top+d:(a=e.absoluteMetrics.top+e.viewMetrics.height-b.height-d,f.scrollbar&&f.scrollbar.canScrollHorizontally()&&(a-=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness));return new Rect$$module$build$src$core$utils$rect(a,a+b.height,c,c+b.width)},getCornerOppositeToolbox$$module$build$src$core$positionable_helpers=function(a,b){return{horizontal:b.toolboxMetrics.position===Position$$module$build$src$core$utils$toolbox.LEFT|| +a.horizontalLayout&&!a.RTL?horizontalPosition$$module$build$src$core$positionable_helpers.RIGHT:horizontalPosition$$module$build$src$core$positionable_helpers.LEFT,vertical:b.toolboxMetrics.position===Position$$module$build$src$core$utils$toolbox.BOTTOM?verticalPosition$$module$build$src$core$positionable_helpers.TOP:verticalPosition$$module$build$src$core$positionable_helpers.BOTTOM}},bumpPositionRect$$module$build$src$core$positionable_helpers=function(a,b,c,d){const e=a.left,f=a.right-a.left,g= +a.bottom-a.top;for(let h=0;hthis.maxDisplayLength&&(a=a.substring(0,this.maxDisplayLength-2)+"\u2026"); -a=a.replace(/\s/g,Field$$module$build$src$core$field.NBSP);this.sourceBlock_&&this.sourceBlock_.RTL&&(a+="\u200f");return a}getText(){const a=this.getText_();return null!==a?String(a):String(this.getValue())}getText_(){return null}markDirty(){this.isDirty_=!0;this.constants_=null}forceRerender(){this.isDirty_=!0;this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours(),this.updateMarkers_())}setValue(a){if(null!==a){var b=this.doClassValidation_(a); -a=this.processValidation_(a,b);if(!(a instanceof Error)){if(b=this.getValidator())if(b=b.call(this,a),a=this.processValidation_(a,b),a instanceof Error)return;b=this.sourceBlock_;if(!b||!b.disposed){var c=this.getValue();c===a?this.doValueUpdate_(a):(this.doValueUpdate_(a),b&&isEnabled$$module$build$src$core$events$utils()&&fire$$module$build$src$core$events$utils(new (get$$module$build$src$core$events$utils(CHANGE$$module$build$src$core$events$utils))(b,"field",this.name||null,c,a)),this.isDirty_&& -this.forceRerender())}}}}processValidation_(a,b){if(null===b)return this.doValueInvalid_(a),this.isDirty_&&this.forceRerender(),Error();void 0!==b&&(a=b);return a}getValue(){return this.value_}doClassValidation_(a){return null===a||void 0===a?null:a}doValueUpdate_(a){this.value_=a;this.isDirty_=!0}doValueInvalid_(a){}onMouseDown_(a){this.sourceBlock_&&!this.sourceBlock_.isDeadOrDying()&&(a=this.sourceBlock_.workspace.getGesture(a))&&a.setStartField(this)}setTooltip(a){a||""===a||(a=this.sourceBlock_); -const b=this.getClickTarget_();b?b.tooltip=a:this.tooltip_=a}getTooltip(){const a=this.getClickTarget_();return a?getTooltipOfObject$$module$build$src$core$tooltip(a):getTooltipOfObject$$module$build$src$core$tooltip({tooltip:this.tooltip_})}getClickTarget_(){return this.clickTarget_||this.getSvgRoot()}getAbsoluteXY_(){return getPageOffset$$module$build$src$core$utils$style(this.getClickTarget_())}referencesVariables(){return!1}refreshVariableName(){}getParentInput(){let a=null;const b=this.getSourceBlock(), -c=b.inputList;for(let d=0;dthis.maxDisplayLength&&(a=a.substring(0,this.maxDisplayLength-2)+"\u2026");a=a.replace(/\s/g,Field$$module$build$src$core$field.NBSP);this.sourceBlock_&&this.sourceBlock_.RTL&&(a+="\u200f");return a}getText(){const a=this.getText_();return null!==a?String(a):String(this.getValue())}getText_(){return null}markDirty(){this.isDirty_=!0;this.constants_=null}forceRerender(){this.isDirty_=!0;this.sourceBlock_&&this.sourceBlock_.rendered&&(this.sourceBlock_.render(),this.sourceBlock_.bumpNeighbours(), +this.updateMarkers_())}setValue(a){if(null!==a){var b=this.doClassValidation_(a);a=this.processValidation_(a,b);if(!(a instanceof Error)){if(b=this.getValidator())if(b=b.call(this,a),a=this.processValidation_(a,b),a instanceof Error)return;b=this.sourceBlock_;if(!b||!b.disposed){var c=this.getValue();c===a?this.doValueUpdate_(a):(this.doValueUpdate_(a),b&&isEnabled$$module$build$src$core$events$utils()&&fire$$module$build$src$core$events$utils(new (get$$module$build$src$core$events$utils(CHANGE$$module$build$src$core$events$utils))(b, +"field",this.name||null,c,a)),this.isDirty_&&this.forceRerender())}}}}processValidation_(a,b){if(null===b)return this.doValueInvalid_(a),this.isDirty_&&this.forceRerender(),Error();void 0!==b&&(a=b);return a}getValue(){return this.value_}doClassValidation_(a){return null===a||void 0===a?null:a}doValueUpdate_(a){this.value_=a;this.isDirty_=!0}doValueInvalid_(a){}onMouseDown_(a){this.sourceBlock_&&!this.sourceBlock_.isDeadOrDying()&&(a=this.sourceBlock_.workspace.getGesture(a))&&a.setStartField(this)}setTooltip(a){a|| +""===a||(a=this.sourceBlock_);const b=this.getClickTarget_();b?b.tooltip=a:this.tooltip_=a}getTooltip(){const a=this.getClickTarget_();return a?getTooltipOfObject$$module$build$src$core$tooltip(a):getTooltipOfObject$$module$build$src$core$tooltip({tooltip:this.tooltip_})}getClickTarget_(){return this.clickTarget_||this.getSvgRoot()}getAbsoluteXY_(){return getPageOffset$$module$build$src$core$utils$style(this.getClickTarget_())}referencesVariables(){return!1}refreshVariableName(){}getParentInput(){let a= +null;const b=this.getSourceBlock();if(!b)throw new UnattachedFieldError$$module$build$src$core$field;const c=b.inputList;for(let d=0;da.length)){b=[];for(c=0;ca.length)){b=[];for(c=0;c=a&&this.getSourceBlock().outputConnection&&!b}else this.fullBlockClickTarget_=!1;this.fullBlockClickTarget_?this.clickTarget_=this.sourceBlock_.getSvgRoot():this.createBorderRect_();this.createTextElement_()}doClassValidation_(a){return null=== +void 0!==a.spellcheck&&(this.spellcheck_=a.spellcheck)}initView(){const a=this.getSourceBlock();if(!a)throw new UnattachedFieldError$$module$build$src$core$field;if(this.getConstants().FULL_BLOCK_FIELDS){let b=0,c=0;for(let d=0,e;e=a.inputList[d];d++){for(let f=0;e.fieldRow[f];f++)b++;e.connection&&c++}this.fullBlockClickTarget_=1>=b&&a.outputConnection&&!c}else this.fullBlockClickTarget_=!1;this.fullBlockClickTarget_?this.clickTarget_=this.sourceBlock_.getSvgRoot():this.createBorderRect_();this.createTextElement_()}doClassValidation_(a){return null=== a||void 0===a?null:String(a)}doValueInvalid_(a){this.isBeingEdited_&&(this.isTextValid_=!1,a=this.value_,this.value_=this.htmlInput_.getAttribute("data-untyped-default-value"),this.sourceBlock_&&isEnabled$$module$build$src$core$events$utils()&&fire$$module$build$src$core$events$utils(new (get$$module$build$src$core$events$utils(CHANGE$$module$build$src$core$events$utils))(this.sourceBlock_,"field",this.name||null,a,this.value_)))}doValueUpdate_(a){this.isTextValid_=!0;this.value_=a;this.isBeingEdited_|| (this.isDirty_=!0)}applyColour(){if(this.sourceBlock_&&this.getConstants().FULL_BLOCK_FIELDS){var a=this.sourceBlock_;if(this.borderRect_){if(!a.style.colourTertiary)throw Error("The renderer did not properly initialize the block style");this.borderRect_.setAttribute("stroke",a.style.colourTertiary)}else a.pathObject.svgPath.setAttribute("fill",this.getConstants().FIELD_BORDER_RECT_COLOUR)}}render_(){super.render_();if(this.isBeingEdited_){this.resizeEditor_();const a=this.htmlInput_;this.isTextValid_? (removeClass$$module$build$src$core$utils$dom(a,"blocklyInvalidInput"),setState$$module$build$src$core$utils$aria(a,State$$module$build$src$core$utils$aria.INVALID,!1)):(addClass$$module$build$src$core$utils$dom(a,"blocklyInvalidInput"),setState$$module$build$src$core$utils$aria(a,State$$module$build$src$core$utils$aria.INVALID,!0))}}setSpellcheck(a){a!==this.spellcheck_&&(this.spellcheck_=a,this.htmlInput_&&this.htmlInput_.setAttribute("spellcheck",this.spellcheck_))}showEditor_(a,b){this.workspace_= -this.sourceBlock_.workspace;a=b||!1;!a&&(MOBILE$$module$build$src$core$utils$useragent||ANDROID$$module$build$src$core$utils$useragent||IPAD$$module$build$src$core$utils$useragent)?this.showPromptEditor_():this.showInlineEditor_(a)}showPromptEditor_(){prompt$$module$build$src$core$dialog(Msg$$module$build$src$core$msg.CHANGE_VALUE_TITLE,this.getText(),a=>{null!==a&&this.setValue(this.getValueFromEditorText_(a))})}showInlineEditor_(a){show$$module$build$src$core$widgetdiv(this,this.getSourceBlock().RTL, -this.widgetDispose_.bind(this));this.htmlInput_=this.widgetCreate_();this.isBeingEdited_=!0;a||(this.htmlInput_.focus({preventScroll:!0}),this.htmlInput_.select())}widgetCreate_(){setGroup$$module$build$src$core$events$utils(!0);const a=getDiv$$module$build$src$core$widgetdiv();var b=this.getClickTarget_();if(!b)throw Error("A click target has not been set.");addClass$$module$build$src$core$utils$dom(b,"editing");b=document.createElement("input");b.className="blocklyHtmlInput";b.setAttribute("spellcheck", -this.spellcheck_);const c=this.workspace_.getScale();var d=this.getConstants().FIELD_TEXT_FONTSIZE*c+"pt";a.style.fontSize=d;b.style.fontSize=d;d=$.FieldTextInput$$module$build$src$core$field_textinput.BORDERRADIUS*c+"px";if(this.fullBlockClickTarget_){d=this.getScaledBBox();d=(d.bottom-d.top)/2+"px";const e=this.getSourceBlock().getParent()?this.getSourceBlock().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.getConstants().FIELD_TEXTINPUT_BOX_SHADOW&&(a.style.boxShadow="rgba(255, 255, 255, 0.3) 0 0 0 "+4*c+"px")}b.style.borderRadius=d;a.appendChild(b);b.value=b.defaultValue=this.getEditorText_(this.value_);b.setAttribute("data-untyped-default-value",this.value_);b.setAttribute("data-old-value","");this.resizeEditor_();this.bindInputEvents_(b);return b}widgetDispose_(){this.isBeingEdited_=!1;this.isTextValid_=!0;this.forceRerender();this.onFinishEditing_(this.value_);setGroup$$module$build$src$core$events$utils(!1); -this.unbindInputEvents_();var a=getDiv$$module$build$src$core$widgetdiv().style;a.width="auto";a.height="auto";a.fontSize="";a.transition="";a.boxShadow="";this.htmlInput_=null;a=this.getClickTarget_();if(!a)throw Error("A click target has not been set.");removeClass$$module$build$src$core$utils$dom(a,"editing")}onFinishEditing_(a){}bindInputEvents_(a){this.onKeyDownWrapper_=conditionalBind$$module$build$src$core$browser_events(a,"keydown",this,this.onHtmlInputKeyDown_);this.onKeyInputWrapper_=conditionalBind$$module$build$src$core$browser_events(a, -"input",this,this.onHtmlInputChange_)}unbindInputEvents_(){this.onKeyDownWrapper_&&(unbind$$module$build$src$core$browser_events(this.onKeyDownWrapper_),this.onKeyDownWrapper_=null);this.onKeyInputWrapper_&&(unbind$$module$build$src$core$browser_events(this.onKeyInputWrapper_),this.onKeyInputWrapper_=null)}onHtmlInputKeyDown_(a){a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.ENTER?(hide$$module$build$src$core$widgetdiv(),hideWithoutAnimation$$module$build$src$core$dropdowndiv()):a.keyCode=== -KeyCodes$$module$build$src$core$utils$keycodes.ESC?(this.setValue(this.htmlInput_.getAttribute("data-untyped-default-value")),hide$$module$build$src$core$widgetdiv(),hideWithoutAnimation$$module$build$src$core$dropdowndiv()):a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.TAB&&(hide$$module$build$src$core$widgetdiv(),hideWithoutAnimation$$module$build$src$core$dropdowndiv(),this.sourceBlock_.tab(this,!a.shiftKey),a.preventDefault())}onHtmlInputChange_(a){a=this.htmlInput_.value;a!==this.htmlInput_.getAttribute("data-old-value")&& -(this.htmlInput_.setAttribute("data-old-value",a),a=this.getValueFromEditorText_(a),this.setValue(a),this.forceRerender(),this.resizeEditor_())}setEditorValue_(a){this.isDirty_=!0;this.isBeingEdited_&&(this.htmlInput_.value=this.getEditorText_(a));this.setValue(a)}resizeEditor_(){const a=getDiv$$module$build$src$core$widgetdiv();var b=this.getScaledBBox();a.style.width=b.right-b.left+"px";a.style.height=b.bottom-b.top+"px";const c=this.getSourceBlock().RTL?b.right-a.offsetWidth:b.left;b=new Coordinate$$module$build$src$core$utils$coordinate(c, -b.top);a.style.left=b.x+"px";a.style.top=b.y+"px"}isTabNavigable(){return!0}getText_(){return this.isBeingEdited_&&this.htmlInput_?this.htmlInput_.value:null}getEditorText_(a){return String(a)}getValueFromEditorText_(a){return a}static fromJson(a){return new this(replaceMessageReferences$$module$build$src$core$utils$parsing(a.text),void 0,a)}};$.FieldTextInput$$module$build$src$core$field_textinput.BORDERRADIUS=4;register$$module$build$src$core$field_registry("field_input",$.FieldTextInput$$module$build$src$core$field_textinput); -$.FieldTextInput$$module$build$src$core$field_textinput.prototype.DEFAULT_VALUE="";var module$build$src$core$field_textinput={};module$build$src$core$field_textinput.FieldTextInput=$.FieldTextInput$$module$build$src$core$field_textinput;var BottomRow$$module$build$src$core$renderers$zelos$measurables$bottom_row=class extends BottomRow$$module$build$src$core$renderers$measurables$bottom_row{constructor(a){super(a)}endsWithElemSpacer(){return!1}hasLeftSquareCorner(a){return!!a.outputConnection}hasRightSquareCorner(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection}},module$build$src$core$renderers$zelos$measurables$bottom_row={};module$build$src$core$renderers$zelos$measurables$bottom_row.BottomRow=BottomRow$$module$build$src$core$renderers$zelos$measurables$bottom_row;var StatementInput$$module$build$src$core$renderers$zelos$measurables$inputs=class extends StatementInput$$module$build$src$core$renderers$measurables$statement_input{constructor(a,b){super(a,b);this.connectedBottomNextConnection=!1;if(this.connectedBlock){for(a=this.connectedBlock;b=a.getNextBlock();)a=b;a.nextConnection||(this.height=this.connectedBlockHeight,this.connectedBottomNextConnection=!0)}}},module$build$src$core$renderers$zelos$measurables$inputs={}; +this.sourceBlock_.workspace;a=b||!1;!a&&(MOBILE$$module$build$src$core$utils$useragent||ANDROID$$module$build$src$core$utils$useragent||IPAD$$module$build$src$core$utils$useragent)?this.showPromptEditor_():this.showInlineEditor_(a)}showPromptEditor_(){prompt$$module$build$src$core$dialog(Msg$$module$build$src$core$msg.CHANGE_VALUE_TITLE,this.getText(),a=>{null!==a&&this.setValue(this.getValueFromEditorText_(a))})}showInlineEditor_(a){const b=this.getSourceBlock();if(!b)throw new UnattachedFieldError$$module$build$src$core$field; +show$$module$build$src$core$widgetdiv(this,b.RTL,this.widgetDispose_.bind(this));this.htmlInput_=this.widgetCreate_();this.isBeingEdited_=!0;a||(this.htmlInput_.focus({preventScroll:!0}),this.htmlInput_.select())}widgetCreate_(){var a=this.getSourceBlock();if(!a)throw new UnattachedFieldError$$module$build$src$core$field;setGroup$$module$build$src$core$events$utils(!0);const b=getDiv$$module$build$src$core$widgetdiv();var c=this.getClickTarget_();if(!c)throw Error("A click target has not been set."); +addClass$$module$build$src$core$utils$dom(c,"editing");c=document.createElement("input");c.className="blocklyHtmlInput";c.setAttribute("spellcheck",this.spellcheck_);const d=this.workspace_.getScale();var e=this.getConstants().FIELD_TEXT_FONTSIZE*d+"pt";b.style.fontSize=e;c.style.fontSize=e;e=$.FieldTextInput$$module$build$src$core$field_textinput.BORDERRADIUS*d+"px";this.fullBlockClickTarget_&&(e=this.getScaledBBox(),e=(e.bottom-e.top)/2+"px",a=a.getParent()?a.getParent().style.colourTertiary:this.sourceBlock_.style.colourTertiary, +c.style.border=1*d+"px solid "+a,b.style.borderRadius=e,b.style.transition="box-shadow 0.25s ease 0s",this.getConstants().FIELD_TEXTINPUT_BOX_SHADOW&&(b.style.boxShadow="rgba(255, 255, 255, 0.3) 0 0 0 "+4*d+"px"));c.style.borderRadius=e;b.appendChild(c);c.value=c.defaultValue=this.getEditorText_(this.value_);c.setAttribute("data-untyped-default-value",this.value_);c.setAttribute("data-old-value","");this.resizeEditor_();this.bindInputEvents_(c);return c}widgetDispose_(){this.isBeingEdited_=!1;this.isTextValid_= +!0;this.forceRerender();this.onFinishEditing_(this.value_);setGroup$$module$build$src$core$events$utils(!1);this.unbindInputEvents_();var a=getDiv$$module$build$src$core$widgetdiv().style;a.width="auto";a.height="auto";a.fontSize="";a.transition="";a.boxShadow="";this.htmlInput_=null;a=this.getClickTarget_();if(!a)throw Error("A click target has not been set.");removeClass$$module$build$src$core$utils$dom(a,"editing")}onFinishEditing_(a){}bindInputEvents_(a){this.onKeyDownWrapper_=conditionalBind$$module$build$src$core$browser_events(a, +"keydown",this,this.onHtmlInputKeyDown_);this.onKeyInputWrapper_=conditionalBind$$module$build$src$core$browser_events(a,"input",this,this.onHtmlInputChange_)}unbindInputEvents_(){this.onKeyDownWrapper_&&(unbind$$module$build$src$core$browser_events(this.onKeyDownWrapper_),this.onKeyDownWrapper_=null);this.onKeyInputWrapper_&&(unbind$$module$build$src$core$browser_events(this.onKeyInputWrapper_),this.onKeyInputWrapper_=null)}onHtmlInputKeyDown_(a){a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.ENTER? +(hide$$module$build$src$core$widgetdiv(),hideWithoutAnimation$$module$build$src$core$dropdowndiv()):a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.ESC?(this.setValue(this.htmlInput_.getAttribute("data-untyped-default-value")),hide$$module$build$src$core$widgetdiv(),hideWithoutAnimation$$module$build$src$core$dropdowndiv()):a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.TAB&&(hide$$module$build$src$core$widgetdiv(),hideWithoutAnimation$$module$build$src$core$dropdowndiv(),this.sourceBlock_.tab(this, +!a.shiftKey),a.preventDefault())}onHtmlInputChange_(a){a=this.htmlInput_.value;a!==this.htmlInput_.getAttribute("data-old-value")&&(this.htmlInput_.setAttribute("data-old-value",a),a=this.getValueFromEditorText_(a),this.setValue(a),this.forceRerender(),this.resizeEditor_())}setEditorValue_(a){this.isDirty_=!0;this.isBeingEdited_&&(this.htmlInput_.value=this.getEditorText_(a));this.setValue(a)}resizeEditor_(){var a=this.getSourceBlock();if(!a)throw new UnattachedFieldError$$module$build$src$core$field; +const b=getDiv$$module$build$src$core$widgetdiv(),c=this.getScaledBBox();b.style.width=c.right-c.left+"px";b.style.height=c.bottom-c.top+"px";a=new Coordinate$$module$build$src$core$utils$coordinate(a.RTL?c.right-b.offsetWidth:c.left,c.top);b.style.left=a.x+"px";b.style.top=a.y+"px"}isTabNavigable(){return!0}getText_(){return this.isBeingEdited_&&this.htmlInput_?this.htmlInput_.value:null}getEditorText_(a){return String(a)}getValueFromEditorText_(a){return a}static fromJson(a){return new this(replaceMessageReferences$$module$build$src$core$utils$parsing(a.text), +void 0,a)}};$.FieldTextInput$$module$build$src$core$field_textinput.BORDERRADIUS=4;register$$module$build$src$core$field_registry("field_input",$.FieldTextInput$$module$build$src$core$field_textinput);$.FieldTextInput$$module$build$src$core$field_textinput.prototype.DEFAULT_VALUE="";var module$build$src$core$field_textinput={};module$build$src$core$field_textinput.FieldTextInput=$.FieldTextInput$$module$build$src$core$field_textinput;var BottomRow$$module$build$src$core$renderers$zelos$measurables$bottom_row=class extends BottomRow$$module$build$src$core$renderers$measurables$bottom_row{constructor(a){super(a)}endsWithElemSpacer(){return!1}hasLeftSquareCorner(a){return!!a.outputConnection}hasRightSquareCorner(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection}},module$build$src$core$renderers$zelos$measurables$bottom_row={};module$build$src$core$renderers$zelos$measurables$bottom_row.BottomRow=BottomRow$$module$build$src$core$renderers$zelos$measurables$bottom_row;var StatementInput$$module$build$src$core$renderers$zelos$measurables$inputs=class extends StatementInput$$module$build$src$core$renderers$measurables$statement_input{constructor(a,b){super(a,b);this.connectedBottomNextConnection=!1;if(this.connectedBlock){for(a=this.connectedBlock;b=a.getNextBlock();)a=b;a.nextConnection||(this.height=this.connectedBlockHeight,this.connectedBottomNextConnection=!0)}}},module$build$src$core$renderers$zelos$measurables$inputs={}; module$build$src$core$renderers$zelos$measurables$inputs.StatementInput=StatementInput$$module$build$src$core$renderers$zelos$measurables$inputs;var RightConnectionShape$$module$build$src$core$renderers$zelos$measurables$row_elements=class extends Measurable$$module$build$src$core$renderers$measurables$base{constructor(a){super(a);this.width=this.height=0;this.type|=Types$$module$build$src$core$renderers$measurables$types.getType("RIGHT_CONNECTION")}},module$build$src$core$renderers$zelos$measurables$row_elements={};module$build$src$core$renderers$zelos$measurables$row_elements.RightConnectionShape=RightConnectionShape$$module$build$src$core$renderers$zelos$measurables$row_elements;var TopRow$$module$build$src$core$renderers$zelos$measurables$top_row=class extends TopRow$$module$build$src$core$renderers$measurables$top_row{constructor(a){super(a)}endsWithElemSpacer(){return!1}hasLeftSquareCorner(a){const b=(a.hat?"cap"===a.hat:this.constants_.ADD_START_HATS)&&!a.outputConnection&&!a.previousConnection;return!!a.outputConnection||b}hasRightSquareCorner(a){return!!a.outputConnection&&!a.statementInputCount&&!a.nextConnection}},module$build$src$core$renderers$zelos$measurables$top_row= {};module$build$src$core$renderers$zelos$measurables$top_row.TopRow=TopRow$$module$build$src$core$renderers$zelos$measurables$top_row;var RenderInfo$$module$build$src$core$renderers$zelos$info=class extends RenderInfo$$module$build$src$core$renderers$common$info{constructor(a,b){super(a,b);this.isInline=!0;this.renderer_=a;this.constants_=this.renderer_.getConstants();this.topRow=new TopRow$$module$build$src$core$renderers$zelos$measurables$top_row(this.constants_);this.bottomRow=new BottomRow$$module$build$src$core$renderers$zelos$measurables$bottom_row(this.constants_);this.isMultiRow=!b.getInputsInline()||b.isCollapsed();this.hasStatementInput= 090-b||a>-90-b&&a<-90+b?!0:!1}getClientRect(){if(!this.svgGroup_||this.autoClose||!this.isVisible())return null;const a=this.svgGroup_.getBoundingClientRect(),b=a.top;return this.toolboxPosition_===Position$$module$build$src$core$utils$toolbox.TOP?new Rect$$module$build$src$core$utils$rect(-1E9,b+a.height,-1E9,1E9):new Rect$$module$build$src$core$utils$rect(b,1E9,-1E9,1E9)}reflowInternal_(){this.workspace_.scale=this.getFlyoutScale();let a=0;const b=this.workspace_.getTopBlocks(!1);for(let d= 0,e;e=b[d];d++)a=Math.max(a,e.getHeightWidth().height);const c=this.buttons_;for(let d=0,e;e=c[d];d++)a=Math.max(a,e.height);a+=1.5*this.MARGIN;a*=this.workspace_.scale;a+=Scrollbar$$module$build$src$core$scrollbar.scrollbarThickness;if(this.height_!==a){for(let d=0,e;e=b[d];d++)this.rectMap_.has(e)&&this.moveRectToBlock_(this.rectMap_.get(e),e);this.targetWorkspace.toolboxPosition!==this.toolboxPosition_||this.toolboxPosition_!==Position$$module$build$src$core$utils$toolbox.TOP||this.targetWorkspace.getToolbox()|| this.targetWorkspace.translate(this.targetWorkspace.scrollX,this.targetWorkspace.scrollY+a);this.height_=a;this.position();this.targetWorkspace.recordDragTargets()}}};register$$module$build$src$core$registry(Type$$module$build$src$core$registry.FLYOUTS_HORIZONTAL_TOOLBOX,DEFAULT$$module$build$src$core$registry,HorizontalFlyout$$module$build$src$core$flyout_horizontal);var module$build$src$core$flyout_horizontal={};module$build$src$core$flyout_horizontal.HorizontalFlyout=HorizontalFlyout$$module$build$src$core$flyout_horizontal;var FieldVariable$$module$build$src$core$field_variable=class extends FieldDropdown$$module$build$src$core$field_dropdown{constructor(a,b,c,d,e){super(Field$$module$build$src$core$field.SKIP_SETUP);this.defaultType_="";this.variableTypes=[];this.variable_=null;this.SERIALIZABLE=!0;this.menuGenerator_=FieldVariable$$module$build$src$core$field_variable.dropdownCreate;this.defaultVariableName="string"===typeof a?a:"";this.size_=new Size$$module$build$src$core$utils$size(0,0);a!==Field$$module$build$src$core$field.SKIP_SETUP&& -(e?this.configure_(e):this.setTypes_(c,d),b&&this.setValidator(b))}configure_(a){super.configure_(a);this.setTypes_(a.variableTypes,a.defaultType)}initModel(){if(!this.variable_){var a=getOrCreateVariablePackage$$module$build$src$core$variables(this.getSourceBlock().workspace,null,this.defaultVariableName,this.defaultType_);this.doValueUpdate_(a.getId())}}shouldAddBorderRect_(){return super.shouldAddBorderRect_()&&(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW||"variables_get"!==this.getSourceBlock().type)}fromXml(a){var b= -a.getAttribute("id");const c=a.textContent,d=a.getAttribute("variabletype")||a.getAttribute("variableType")||"";b=getOrCreateVariablePackage$$module$build$src$core$variables(this.getSourceBlock().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: "+domToText$$module$build$src$core$xml(a)+".");this.setValue(b.getId())}toXml(a){this.initModel();a.id=this.variable_.getId();a.textContent= -this.variable_.name;this.variable_.type&&a.setAttribute("variabletype",this.variable_.type);return a}saveState(a){var b=this.saveLegacyState(FieldVariable$$module$build$src$core$field_variable);if(null!==b)return b;this.initModel();b={id:this.variable_.getId()};a&&(b.name=this.variable_.name,b.type=this.variable_.type);return b}loadState(a){this.loadLegacyState(FieldVariable$$module$build$src$core$field_variable,a)||(a=getOrCreateVariablePackage$$module$build$src$core$variables(this.getSourceBlock().workspace, -a.id||null,a.name,a.type||""),this.setValue(a.getId()))}setSourceBlock(a){if(a.isShadow())throw Error("Variable fields are not allowed to exist on shadow blocks.");super.setSourceBlock(a)}getValue(){return this.variable_?this.variable_.getId():null}getText(){return this.variable_?this.variable_.name:""}getVariable(){return this.variable_}getValidator(){return this.variable_?this.validator_:null}doClassValidation_(a){if(null===a)return null;var b=getVariable$$module$build$src$core$variables(this.getSourceBlock().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)}doValueUpdate_(a){this.variable_=getVariable$$module$build$src$core$variables(this.getSourceBlock().workspace,a);super.doValueUpdate_(a)}typeIsAllowed_(a){const b=this.getVariableTypes_();if(!b)return!0;for(let c=0;cthis.max_&&setState$$module$build$src$core$utils$aria(a, State$$module$build$src$core$utils$aria.VALUEMAX,this.max_);return a}static fromJson(a){return new this(a.value,void 0,void 0,void 0,void 0,a)}};register$$module$build$src$core$field_registry("field_number",FieldNumber$$module$build$src$core$field_number);FieldNumber$$module$build$src$core$field_number.prototype.DEFAULT_VALUE=0;var module$build$src$core$field_number={};module$build$src$core$field_number.FieldNumber=FieldNumber$$module$build$src$core$field_number;var FieldMultilineInput$$module$build$src$core$field_multilineinput=class extends $.FieldTextInput$$module$build$src$core$field_textinput{constructor(a,b,c){super(Field$$module$build$src$core$field.SKIP_SETUP);this.textGroup_=null;this.maxLines_=Infinity;this.isOverflowedY_=!1;a!==Field$$module$build$src$core$field.SKIP_SETUP&&(c&&this.configure_(c),this.setValue(a),b&&this.setValidator(b))}configure_(a){super.configure_(a);a.maxLines&&this.setMaxLines(a.maxLines)}toXml(a){a.textContent=this.getValue().replace(/\n/g, -" ");return a}fromXml(a){this.setValue(a.textContent.replace(/ /g,"\n"))}saveState(){const a=this.saveLegacyState(FieldMultilineInput$$module$build$src$core$field_multilineinput);return null!==a?a:this.getValue()}loadState(a){this.loadLegacyState(Field$$module$build$src$core$field,a)||this.setValue(a)}initView(){this.createBorderRect_();this.textGroup_=createSvgElement$$module$build$src$core$utils$dom(Svg$$module$build$src$core$utils$svg.G,{"class":"blocklyEditableText"},this.fieldGroup_)}getDisplayText_(){let a= -this.getText();if(!a)return Field$$module$build$src$core$field.NBSP;const b=a.split("\n");a="";const c=this.isOverflowedY_?this.maxLines_:b.length;for(let d=0;dthis.maxDisplayLength?e=e.substring(0,this.maxDisplayLength-4)+"...":this.isOverflowedY_&&d===c-1&&(e=e.substring(0,e.length-3)+"...");e=e.replace(/\s/g,Field$$module$build$src$core$field.NBSP);a+=e;d!==c-1&&(a+="\n")}this.getSourceBlock().RTL&&(a+="\u200f");return a}doValueUpdate_(a){super.doValueUpdate_(a);this.isOverflowedY_= -this.value_.split("\n").length>this.maxLines_}render_(){for(var a;a=this.textGroup_.firstChild;)this.textGroup_.removeChild(a);a=this.getDisplayText_().split("\n");let b=0;for(let c=0;ce&&(e=h);f+=this.getConstants().FIELD_TEXT_HEIGHT+(0this.maxDisplayLength&&(a[h]=a[h].substring(0,this.maxDisplayLength)); -g.textContent=a[h];const k=getFastTextWidth$$module$build$src$core$utils$dom(g,b,c,d);k>e&&(e=k)}e+=this.htmlInput_.offsetWidth-this.htmlInput_.clientWidth}this.borderRect_&&(f+=2*this.getConstants().FIELD_BORDER_RECT_Y_PADDING,e+=2*this.getConstants().FIELD_BORDER_RECT_X_PADDING,this.borderRect_.setAttribute("width",e),this.borderRect_.setAttribute("height",f));this.size_.width=e;this.size_.height=f;this.positionBorderRect_()}showEditor_(a,b){super.showEditor_(a,b);this.forceRerender()}widgetCreate_(){const a= -getDiv$$module$build$src$core$widgetdiv(),b=this.workspace_.getScale(),c=document.createElement("textarea");c.className="blocklyHtmlInput blocklyHtmlTextAreaInput";c.setAttribute("spellcheck",this.spellcheck_);var d=this.getConstants().FIELD_TEXT_FONTSIZE*b+"pt";a.style.fontSize=d;c.style.fontSize=d;c.style.borderRadius=$.FieldTextInput$$module$build$src$core$field_textinput.BORDERRADIUS*b+"px";d=this.getConstants().FIELD_BORDER_RECT_X_PADDING*b;const e=this.getConstants().FIELD_BORDER_RECT_Y_PADDING* -b/2;c.style.padding=e+"px "+d+"px "+e+"px "+d+"px";d=this.getConstants().FIELD_TEXT_HEIGHT+this.getConstants().FIELD_BORDER_RECT_Y_PADDING;c.style.lineHeight=d*b+"px";a.appendChild(c);c.value=c.defaultValue=this.getEditorText_(this.value_);c.setAttribute("data-untyped-default-value",this.value_);c.setAttribute("data-old-value","");GECKO$$module$build$src$core$utils$useragent?setTimeout(this.resizeEditor_.bind(this),0):this.resizeEditor_();this.bindInputEvents_(c);return c}setMaxLines(a){"number"=== -typeof a&&0this.maxDisplayLength?f=f.substring(0,this.maxDisplayLength-4)+"...":this.isOverflowedY_&&e===d-1&&(f=f.substring(0,f.length-3)+"...");f=f.replace(/\s/g,Field$$module$build$src$core$field.NBSP);b+=f;e!==d-1&&(b+="\n")}a.RTL&& +(b+="\u200f");return b}doValueUpdate_(a){super.doValueUpdate_(a);this.isOverflowedY_=this.value_.split("\n").length>this.maxLines_}render_(){var a=this.getSourceBlock();if(!a)throw new UnattachedFieldError$$module$build$src$core$field;for(var b;b=this.textGroup_.firstChild;)this.textGroup_.removeChild(b);b=this.getDisplayText_().split("\n");let c=0;for(let d=0;de&&(e=h);f+=this.getConstants().FIELD_TEXT_HEIGHT+(0this.maxDisplayLength&&(a[h]=a[h].substring(0,this.maxDisplayLength));g.textContent=a[h];const k=getFastTextWidth$$module$build$src$core$utils$dom(g,b,c,d);k>e&&(e=k)}e+=this.htmlInput_.offsetWidth-this.htmlInput_.clientWidth}this.borderRect_&&(f+=2*this.getConstants().FIELD_BORDER_RECT_Y_PADDING,e+=2*this.getConstants().FIELD_BORDER_RECT_X_PADDING,this.borderRect_.setAttribute("width",e),this.borderRect_.setAttribute("height",f));this.size_.width=e;this.size_.height= +f;this.positionBorderRect_()}showEditor_(a,b){super.showEditor_(a,b);this.forceRerender()}widgetCreate_(){const a=getDiv$$module$build$src$core$widgetdiv(),b=this.workspace_.getScale(),c=document.createElement("textarea");c.className="blocklyHtmlInput blocklyHtmlTextAreaInput";c.setAttribute("spellcheck",this.spellcheck_);var d=this.getConstants().FIELD_TEXT_FONTSIZE*b+"pt";a.style.fontSize=d;c.style.fontSize=d;c.style.borderRadius=$.FieldTextInput$$module$build$src$core$field_textinput.BORDERRADIUS* +b+"px";d=this.getConstants().FIELD_BORDER_RECT_X_PADDING*b;const e=this.getConstants().FIELD_BORDER_RECT_Y_PADDING*b/2;c.style.padding=e+"px "+d+"px "+e+"px "+d+"px";d=this.getConstants().FIELD_TEXT_HEIGHT+this.getConstants().FIELD_BORDER_RECT_Y_PADDING;c.style.lineHeight=d*b+"px";a.appendChild(c);c.value=c.defaultValue=this.getEditorText_(this.value_);c.setAttribute("data-untyped-default-value",this.value_);c.setAttribute("data-old-value","");GECKO$$module$build$src$core$utils$useragent?setTimeout(this.resizeEditor_.bind(this), +0):this.resizeEditor_();this.bindInputEvents_(c);return c}setMaxLines(a){"number"===typeof a&&0c?b+=180:0a&&(a+=360);a>this.wrap_&&(a-=360);return a}static fromJson(a){return new this(a.angle,void 0,a)}};FieldAngle$$module$build$src$core$field_angle.ROUND=15;FieldAngle$$module$build$src$core$field_angle.HALF=50;FieldAngle$$module$build$src$core$field_angle.CLOCKWISE=!1;FieldAngle$$module$build$src$core$field_angle.OFFSET=0; -FieldAngle$$module$build$src$core$field_angle.WRAP=360;FieldAngle$$module$build$src$core$field_angle.RADIUS=FieldAngle$$module$build$src$core$field_angle.HALF-1;register$$module$build$src$core$css("\n.blocklyAngleCircle {\n stroke: #444;\n stroke-width: 1;\n fill: #ddd;\n fill-opacity: .8;\n}\n\n.blocklyAngleMarks {\n stroke: #444;\n stroke-width: 1;\n}\n\n.blocklyAngleGauge {\n fill: #f88;\n fill-opacity: .8;\n pointer-events: none;\n}\n\n.blocklyAngleLine {\n stroke: #f00;\n stroke-width: 2;\n stroke-linecap: round;\n pointer-events: none;\n}\n"); +a.join(""));this.line_.setAttribute("x2",c);this.line_.setAttribute("y2",d)}}onHtmlInputKeyDown_(a){super.onHtmlInputKeyDown_(a);var b=this.getSourceBlock();if(!b)throw new UnattachedFieldError$$module$build$src$core$field;let c;a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.LEFT?c=b.RTL?1:-1:a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.RIGHT?c=b.RTL?-1:1:a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.DOWN?c=-1:a.keyCode===KeyCodes$$module$build$src$core$utils$keycodes.UP&& +(c=1);c&&(b=this.getValue(),this.displayMouseOrKeyboardValue_(b+c*this.round_),a.preventDefault(),a.stopPropagation())}doClassValidation_(a){a=Number(a);return isNaN(a)||!isFinite(a)?null:this.wrapValue_(a)}wrapValue_(a){a%=360;0>a&&(a+=360);a>this.wrap_&&(a-=360);return a}static fromJson(a){return new this(a.angle,void 0,a)}};FieldAngle$$module$build$src$core$field_angle.ROUND=15;FieldAngle$$module$build$src$core$field_angle.HALF=50;FieldAngle$$module$build$src$core$field_angle.CLOCKWISE=!1; +FieldAngle$$module$build$src$core$field_angle.OFFSET=0;FieldAngle$$module$build$src$core$field_angle.WRAP=360;FieldAngle$$module$build$src$core$field_angle.RADIUS=FieldAngle$$module$build$src$core$field_angle.HALF-1;register$$module$build$src$core$css("\n.blocklyAngleCircle {\n stroke: #444;\n stroke-width: 1;\n fill: #ddd;\n fill-opacity: .8;\n}\n\n.blocklyAngleMarks {\n stroke: #444;\n stroke-width: 1;\n}\n\n.blocklyAngleGauge {\n fill: #f88;\n fill-opacity: .8;\n pointer-events: none;\n}\n\n.blocklyAngleLine {\n stroke: #f00;\n stroke-width: 2;\n stroke-linecap: round;\n pointer-events: none;\n}\n"); register$$module$build$src$core$field_registry("field_angle",FieldAngle$$module$build$src$core$field_angle);FieldAngle$$module$build$src$core$field_angle.prototype.DEFAULT_VALUE=0;var Mode$$module$build$src$core$field_angle;(function(a){a.COMPASS="compass";a.PROTRACTOR="protractor"})(Mode$$module$build$src$core$field_angle||(Mode$$module$build$src$core$field_angle={}));var module$build$src$core$field_angle={};module$build$src$core$field_angle.FieldAngle=FieldAngle$$module$build$src$core$field_angle; module$build$src$core$field_angle.Mode=Mode$$module$build$src$core$field_angle;var BlockMove$$module$build$src$core$events$events_block_move=class extends BlockBase$$module$build$src$core$events$events_block_base{constructor(a){super(a);this.type=MOVE$$module$build$src$core$events$utils;a&&(a.isShadow()&&(this.recordUndo=!1),a=this.currentLocation_(),this.oldParentId=a.parentId,this.oldInputName=a.inputName,this.oldCoordinate=a.coordinate)}toJson(){const a=super.toJson();a.newParentId=this.newParentId;a.newInputName=this.newInputName;this.newCoordinate&&(a.newCoordinate=`${Math.round(this.newCoordinate.x)}, `+ `${Math.round(this.newCoordinate.y)}`);this.recordUndo||(a.recordUndo=this.recordUndo);return a}fromJson(a){super.fromJson(a);this.newParentId=a.newParentId;this.newInputName=a.newInputName;if(a.newCoordinate){const b=a.newCoordinate.split(",");this.newCoordinate=new Coordinate$$module$build$src$core$utils$coordinate(Number(b[0]),Number(b[1]))}void 0!==a.recordUndo&&(this.recordUndo=a.recordUndo)}recordNew(){const a=this.currentLocation_();this.newParentId=a.parentId;this.newInputName=a.inputName; @@ -1482,7 +1485,7 @@ this.pixelsToWorkspaceUnits_(a);const b=Coordinate$$module$build$src$core$utils$ this.draggingBlock_.render();this.draggingBlock_.scheduleSnapAndBump()}fireDragEndEvent_(){const a=new (get$$module$build$src$core$events$utils(BLOCK_DRAG$$module$build$src$core$events$utils))(this.draggingBlock_,!1,this.draggingBlock_.getDescendants(!1));fire$$module$build$src$core$events$utils(a)}updateToolboxStyle_(a){const b=this.workspace_.getToolbox();if(b){const c=this.draggingBlock_.isDeletable()?"blocklyToolboxDelete":"blocklyToolboxGrab";a&&"function"===typeof b.removeStyle?b.removeStyle(c): a||"function"!==typeof b.addStyle||b.addStyle(c)}}fireMoveEvent_(){const a=new (get$$module$build$src$core$events$utils(MOVE$$module$build$src$core$events$utils))(this.draggingBlock_);a.oldCoordinate=this.startXY_;a.recordNew();fire$$module$build$src$core$events$utils(a)}updateCursorDuringBlockDrag_(){this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_)}pixelsToWorkspaceUnits_(a){a=new Coordinate$$module$build$src$core$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}dragIcons_(a){for(let b=0;b void;\n preventDefault: () => void;\n}\n\n/** Length in ms for a touch to become a long press. */\nconst LONGPRESS = 750;\n\n/**\n * Whether touch is enabled in the browser.\n * Copied from Closure's goog.events.BrowserFeature.TOUCH_ENABLED\n */\nexport const TOUCH_ENABLED = 'ontouchstart' in globalThis ||\n !!(globalThis['document'] && document.documentElement &&\n 'ontouchstart' in\n document.documentElement) || // IE10 uses non-standard touch events,\n // so it has a different check.\n !!(globalThis['navigator'] &&\n (globalThis['navigator']['maxTouchPoints'] ||\n (globalThis['navigator'] as any)['msMaxTouchPoints']));\n\n/** Which touch events are we currently paying attention to? */\nlet touchIdentifier_: string|null = null;\n\n/**\n * The TOUCH_MAP lookup dictionary specifies additional touch events to fire,\n * in conjunction with mouse events.\n *\n * @alias Blockly.Touch.TOUCH_MAP\n */\nexport const TOUCH_MAP: {[key: string]: string[]} = globalThis['PointerEvent'] ?\n {\n 'mousedown': ['pointerdown'],\n 'mouseenter': ['pointerenter'],\n 'mouseleave': ['pointerleave'],\n 'mousemove': ['pointermove'],\n 'mouseout': ['pointerout'],\n 'mouseover': ['pointerover'],\n 'mouseup': ['pointerup', 'pointercancel'],\n 'touchend': ['pointerup'],\n 'touchcancel': ['pointercancel'],\n } :\n {\n 'mousedown': ['touchstart'],\n 'mousemove': ['touchmove'],\n 'mouseup': ['touchend', 'touchcancel'],\n };\n\n/** PID of queued long-press task. */\nlet longPid_: AnyDuringMigration = 0;\n\n/**\n * Context menus on touch devices are activated using a long-press.\n * Unfortunately the contextmenu touch event is currently (2015) only supported\n * by Chrome. This function is fired on any touchstart event, queues a task,\n * which after about a second opens the context menu. The tasks is killed\n * if the touch event terminates early.\n *\n * @param e Touch start event.\n * @param gesture The gesture that triggered this longStart.\n * @alias Blockly.Touch.longStart\n * @internal\n */\nexport function longStart(e: Event, gesture: Gesture) {\n longStop();\n // Punt on multitouch events.\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'Event'.\n if ((e as AnyDuringMigration).changedTouches &&\n (e as AnyDuringMigration).changedTouches.length !== 1) {\n return;\n }\n longPid_ = setTimeout(function() {\n // TODO(#6097): Make types accurate, possibly by refactoring touch handling.\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'Event'.\n const typelessEvent = e as AnyDuringMigration;\n // Additional check to distinguish between touch events and pointer events\n if (typelessEvent.changedTouches) {\n // TouchEvent\n typelessEvent.button = 2; // Simulate a right button click.\n // e was a touch event. It needs to pretend to be a mouse event.\n typelessEvent.clientX = typelessEvent.changedTouches[0].clientX;\n typelessEvent.clientY = typelessEvent.changedTouches[0].clientY;\n }\n\n // Let the gesture route the right-click correctly.\n if (gesture) {\n gesture.handleRightClick(e);\n }\n }, LONGPRESS);\n}\n\n/**\n * Nope, that's not a long-press. Either touchend or touchcancel was fired,\n * or a drag hath begun. Kill the queued long-press task.\n *\n * @alias Blockly.Touch.longStop\n * @internal\n */\nexport function longStop() {\n if (longPid_) {\n clearTimeout(longPid_);\n longPid_ = 0;\n }\n}\n\n/**\n * Clear the touch identifier that tracks which touch stream to pay attention\n * to. This ends the current drag/gesture and allows other pointers to be\n * captured.\n *\n * @alias Blockly.Touch.clearTouchIdentifier\n */\nexport function clearTouchIdentifier() {\n touchIdentifier_ = null;\n}\n\n/**\n * Decide whether Blockly should handle or ignore this event.\n * Mouse and touch events require special checks because we only want to deal\n * with one touch stream at a time. All other events should always be handled.\n *\n * @param e The event to check.\n * @returns True if this event should be passed through to the registered\n * handler; false if it should be blocked.\n * @alias Blockly.Touch.shouldHandleEvent\n */\nexport function shouldHandleEvent(e: Event|PseudoEvent): boolean {\n return !isMouseOrTouchEvent(e) || checkTouchIdentifier(e);\n}\n\n/**\n * Get the touch identifier from the given event. If it was a mouse event, the\n * identifier is the string 'mouse'.\n *\n * @param e Mouse event or touch event.\n * @returns The touch identifier from the first changed touch, if defined.\n * Otherwise 'mouse'.\n * @alias Blockly.Touch.getTouchIdentifierFromEvent\n */\nexport function getTouchIdentifierFromEvent(e: Event|PseudoEvent): string {\n if (e instanceof MouseEvent) {\n return 'mouse';\n }\n\n if (e instanceof PointerEvent) {\n return String(e.pointerId);\n }\n\n /**\n * TODO(#6097): Fix types. This is a catch-all for everything but mouse\n * and pointer events.\n */\n const pseudoEvent = /** {!PseudoEvent} */ e;\n\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'. AnyDuringMigration because: Property\n // 'changedTouches' does not exist on type 'PseudoEvent | Event'.\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'. AnyDuringMigration because: Property\n // 'changedTouches' does not exist on type 'PseudoEvent | Event'.\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'.\n return (pseudoEvent as AnyDuringMigration).changedTouches &&\n (pseudoEvent as AnyDuringMigration).changedTouches[0] &&\n (pseudoEvent as AnyDuringMigration).changedTouches[0].identifier !==\n undefined &&\n (pseudoEvent as AnyDuringMigration).changedTouches[0].identifier !==\n null ?\n String((pseudoEvent as AnyDuringMigration).changedTouches[0].identifier) :\n 'mouse';\n}\n\n/**\n * Check whether the touch identifier on the event matches the current saved\n * identifier. If there is no identifier, that means it's a mouse event and\n * we'll use the identifier \"mouse\". This means we won't deal well with\n * multiple mice being used at the same time. That seems okay.\n * If the current identifier was unset, save the identifier from the\n * event. This starts a drag/gesture, during which touch events with other\n * identifiers will be silently ignored.\n *\n * @param e Mouse event or touch event.\n * @returns Whether the identifier on the event matches the current saved\n * identifier.\n * @alias Blockly.Touch.checkTouchIdentifier\n */\nexport function checkTouchIdentifier(e: Event|PseudoEvent): boolean {\n const identifier = getTouchIdentifierFromEvent(e);\n\n // if (touchIdentifier_) is insufficient because Android touch\n // identifiers may be zero.\n if (touchIdentifier_ !== undefined && touchIdentifier_ !== null) {\n // We're already tracking some touch/mouse event. Is this from the same\n // source?\n return touchIdentifier_ === identifier;\n }\n if (e.type === 'mousedown' || e.type === 'touchstart' ||\n e.type === 'pointerdown') {\n // No identifier set yet, and this is the start of a drag. Set it and\n // return.\n touchIdentifier_ = identifier;\n return true;\n }\n // There was no identifier yet, but this wasn't a start event so we're going\n // to ignore it. This probably means that another drag finished while this\n // pointer was down.\n return false;\n}\n\n/**\n * Set an event's clientX and clientY from its first changed touch. Use this to\n * make a touch event work in a mouse event handler.\n *\n * @param e A touch event.\n * @alias Blockly.Touch.setClientFromTouch\n */\nexport function setClientFromTouch(e: Event|PseudoEvent) {\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'.\n if (e.type.startsWith('touch') && (e as AnyDuringMigration).changedTouches) {\n // Map the touch event's properties to the event.\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'.\n const touchPoint = (e as AnyDuringMigration).changedTouches[0];\n // AnyDuringMigration because: Property 'clientX' does not exist on type\n // 'PseudoEvent | Event'.\n (e as AnyDuringMigration).clientX = touchPoint.clientX;\n // AnyDuringMigration because: Property 'clientY' does not exist on type\n // 'PseudoEvent | Event'.\n (e as AnyDuringMigration).clientY = touchPoint.clientY;\n }\n}\n\n/**\n * Check whether a given event is a mouse, touch, or pointer event.\n *\n * @param e An event.\n * @returns True if it is a mouse, touch, or pointer event; false otherwise.\n * @alias Blockly.Touch.isMouseOrTouchEvent\n */\nexport function isMouseOrTouchEvent(e: Event|PseudoEvent): boolean {\n return e.type.startsWith('touch') || e.type.startsWith('mouse') ||\n e.type.startsWith('pointer');\n}\n\n/**\n * Check whether a given event is a touch event or a pointer event.\n *\n * @param e An event.\n * @returns True if it is a touch or pointer event; false otherwise.\n * @alias Blockly.Touch.isTouchEvent\n */\nexport function isTouchEvent(e: Event|PseudoEvent): boolean {\n return e.type.startsWith('touch') || e.type.startsWith('pointer');\n}\n\n/**\n * Split an event into an array of events, one per changed touch or mouse\n * point.\n *\n * @param e A mouse event or a touch event with one or more changed touches.\n * @returns An array of events or pseudo events.\n * Each pseudo-touch event will have exactly one changed touch and there\n * will be no real touch events.\n * @alias Blockly.Touch.splitEventByTouches\n */\nexport function splitEventByTouches(e: Event): Array {\n const events = [];\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'.\n if ((e as AnyDuringMigration).changedTouches) {\n // AnyDuringMigration because: Property 'changedTouches' does not exist on\n // type 'PseudoEvent | Event'.\n for (let i = 0; i < (e as AnyDuringMigration).changedTouches.length; i++) {\n const newEvent = {\n type: e.type,\n // AnyDuringMigration because: Property 'changedTouches' does not exist\n // on type 'PseudoEvent | Event'.\n changedTouches: [(e as AnyDuringMigration).changedTouches[i]],\n target: e.target,\n stopPropagation() {\n e.stopPropagation();\n },\n preventDefault() {\n e.preventDefault();\n },\n };\n events[i] = newEvent;\n }\n } else {\n events.push(e);\n }\n // AnyDuringMigration because: Type '(Event | { type: string; changedTouches:\n // Touch[]; target: EventTarget | null; stopPropagation(): void;\n // preventDefault(): void; })[]' is not assignable to type '(PseudoEvent |\n // Event)[]'.\n return events as AnyDuringMigration;\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Browser event handling.\n *\n * @namespace Blockly.browserEvents\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.browserEvents');\n\nimport * as Touch from './touch.js';\nimport * as userAgent from './utils/useragent.js';\n\n\n/**\n * Blockly opaque event data used to unbind events when using\n * `bind` and `conditionalBind`.\n *\n * @alias Blockly.browserEvents.Data\n */\nexport type Data = [EventTarget, string, (e: Event) => void][];\n\n/**\n * The multiplier for scroll wheel deltas using the line delta mode.\n * See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode\n * for more information on deltaMode.\n */\nconst LINE_MODE_MULTIPLIER = 40;\n\n/**\n * The multiplier for scroll wheel deltas using the page delta mode.\n * See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode\n * for more information on deltaMode.\n */\nconst PAGE_MODE_MULTIPLIER = 125;\n\n/**\n * Bind an event handler that can be ignored if it is not part of the active\n * touch stream.\n * Use this for events that either start or continue a multi-part gesture (e.g.\n * mousedown or mousemove, which may be part of a drag or click).\n *\n * @param node Node upon which to listen.\n * @param name Event name to listen to (e.g. 'mousedown').\n * @param thisObject The value of 'this' in the function.\n * @param func Function to call when event is triggered.\n * @param opt_noCaptureIdentifier True if triggering on this event should not\n * block execution of other event handlers on this touch or other\n * simultaneous touches. False by default.\n * @param opt_noPreventDefault True if triggering on this event should prevent\n * the default handler. False by default. If opt_noPreventDefault is\n * provided, opt_noCaptureIdentifier must also be provided.\n * @returns Opaque data that can be passed to unbindEvent_.\n * @alias Blockly.browserEvents.conditionalBind\n */\nexport function conditionalBind(\n node: EventTarget, name: string, thisObject: Object|null, func: Function,\n opt_noCaptureIdentifier?: boolean, opt_noPreventDefault?: boolean): Data {\n let handled = false;\n /**\n *\n * @param e\n */\n function wrapFunc(e: Event) {\n const captureIdentifier = !opt_noCaptureIdentifier;\n // Handle each touch point separately. If the event was a mouse event, this\n // will hand back an array with one element, which we're fine handling.\n const events = Touch.splitEventByTouches(e);\n for (let i = 0; i < events.length; i++) {\n const event = events[i];\n if (captureIdentifier && !Touch.shouldHandleEvent(event)) {\n continue;\n }\n Touch.setClientFromTouch(event);\n if (thisObject) {\n func.call(thisObject, event);\n } else {\n func(event);\n }\n handled = true;\n }\n }\n\n const bindData: Data = [];\n if (globalThis['PointerEvent'] && name in Touch.TOUCH_MAP) {\n for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {\n const type = Touch.TOUCH_MAP[name][i];\n node.addEventListener(type, wrapFunc, false);\n bindData.push([node, type, wrapFunc]);\n }\n } else {\n node.addEventListener(name, wrapFunc, false);\n bindData.push([node, name, wrapFunc]);\n\n // Add equivalent touch event.\n if (name in Touch.TOUCH_MAP) {\n const touchWrapFunc = (e: Event) => {\n wrapFunc(e);\n // Calling preventDefault stops the browser from scrolling/zooming the\n // page.\n const preventDef = !opt_noPreventDefault;\n if (handled && preventDef) {\n e.preventDefault();\n }\n };\n for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {\n const type = Touch.TOUCH_MAP[name][i];\n node.addEventListener(type, touchWrapFunc, false);\n bindData.push([node, type, touchWrapFunc]);\n }\n }\n }\n return bindData;\n}\n\n/**\n * Bind an event handler that should be called regardless of whether it is part\n * of the active touch stream.\n * Use this for events that are not part of a multi-part gesture (e.g.\n * mouseover for tooltips).\n *\n * @param node Node upon which to listen.\n * @param name Event name to listen to (e.g. 'mousedown').\n * @param thisObject The value of 'this' in the function.\n * @param func Function to call when event is triggered.\n * @returns Opaque data that can be passed to unbindEvent_.\n * @alias Blockly.browserEvents.bind\n */\nexport function bind(\n node: EventTarget, name: string, thisObject: Object|null,\n func: Function): Data {\n /**\n *\n * @param e\n */\n function wrapFunc(e: Event) {\n if (thisObject) {\n func.call(thisObject, e);\n } else {\n func(e);\n }\n }\n\n const bindData: Data = [];\n if (globalThis['PointerEvent'] && name in Touch.TOUCH_MAP) {\n for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {\n const type = Touch.TOUCH_MAP[name][i];\n node.addEventListener(type, wrapFunc, false);\n bindData.push([node, type, wrapFunc]);\n }\n } else {\n node.addEventListener(name, wrapFunc, false);\n bindData.push([node, name, wrapFunc]);\n\n // Add equivalent touch event.\n if (name in Touch.TOUCH_MAP) {\n const touchWrapFunc = (e: Event) => {\n // Punt on multitouch events.\n if (e instanceof TouchEvent && e.changedTouches &&\n e.changedTouches.length === 1) {\n // Map the touch event's properties to the event.\n const touchPoint = e.changedTouches[0];\n // TODO (6311): We are trying to make a touch event look like a mouse\n // event, which is not allowed, because it requires adding more\n // properties to the event. How do we want to deal with this?\n (e as AnyDuringMigration).clientX = touchPoint.clientX;\n (e as AnyDuringMigration).clientY = touchPoint.clientY;\n }\n wrapFunc(e);\n\n // Stop the browser from scrolling/zooming the page.\n e.preventDefault();\n };\n for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {\n const type = Touch.TOUCH_MAP[name][i];\n node.addEventListener(type, touchWrapFunc, false);\n bindData.push([node, type, touchWrapFunc]);\n }\n }\n }\n return bindData;\n}\n\n/**\n * Unbind one or more events event from a function call.\n *\n * @param bindData Opaque data from bindEvent_.\n * This list is emptied during the course of calling this function.\n * @returns The function call.\n * @alias Blockly.browserEvents.unbind\n */\nexport function unbind(bindData: Data): (e: Event) => void {\n // Accessing an element of the last property of the array is unsafe if the\n // bindData is an empty array. But that should never happen because developers\n // should only pass Data from bind or conditionalBind.\n const callback = bindData[bindData.length - 1][2];\n while (bindData.length) {\n const bindDatum = bindData.pop();\n const node = bindDatum![0];\n const name = bindDatum![1];\n const func = bindDatum![2];\n node.removeEventListener(name, func, false);\n }\n return callback;\n}\n\n/**\n * Returns true if this event is targeting a text input widget?\n *\n * @param e An event.\n * @returns True if text input.\n * @alias Blockly.browserEvents.isTargetInput\n */\nexport function isTargetInput(e: Event): boolean {\n if (e.target instanceof HTMLElement) {\n if (e.target.isContentEditable ||\n e.target.getAttribute('data-is-text-input') === 'true') {\n return true;\n }\n\n if (e.target instanceof HTMLInputElement) {\n const target = e.target;\n return target.type === 'text' || target.type === 'number' ||\n target.type === 'email' || target.type === 'password' ||\n target.type === 'search' || target.type === 'tel' ||\n target.type === 'url';\n }\n\n if (e.target instanceof HTMLTextAreaElement) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Returns true this event is a right-click.\n *\n * @param e Mouse event.\n * @returns True if right-click.\n * @alias Blockly.browserEvents.isRightButton\n */\nexport function isRightButton(e: MouseEvent): boolean {\n if (e.ctrlKey && userAgent.MAC) {\n // Control-clicking on Mac OS X is treated as a right-click.\n // WebKit on Mac OS X fails to change button to 2 (but Gecko does).\n return true;\n }\n return e.button === 2;\n}\n\n/**\n * Returns the converted coordinates of the given mouse event.\n * The origin (0,0) is the top-left corner of the Blockly SVG.\n *\n * @param e Mouse event.\n * @param svg SVG element.\n * @param matrix Inverted screen CTM to use.\n * @returns Object with .x and .y properties.\n * @alias Blockly.browserEvents.mouseToSvg\n */\nexport function mouseToSvg(\n e: MouseEvent, svg: SVGSVGElement, matrix: SVGMatrix|null): SVGPoint {\n const svgPoint = svg.createSVGPoint();\n svgPoint.x = e.clientX;\n svgPoint.y = e.clientY;\n\n if (!matrix) {\n matrix = svg.getScreenCTM()!.inverse();\n }\n return svgPoint.matrixTransform(matrix);\n}\n\n/**\n * Returns the scroll delta of a mouse event in pixel units.\n *\n * @param e Mouse event.\n * @returns Scroll delta object with .x and .y properties.\n * @alias Blockly.browserEvents.getScrollDeltaPixels\n */\nexport function getScrollDeltaPixels(e: WheelEvent): {x: number, y: number} {\n switch (e.deltaMode) {\n case 0x00: // Pixel mode.\n default:\n return {x: e.deltaX, y: e.deltaY};\n case 0x01: // Line mode.\n return {\n x: e.deltaX * LINE_MODE_MULTIPLIER,\n y: e.deltaY * LINE_MODE_MULTIPLIER,\n };\n case 0x02: // Page mode.\n return {\n x: e.deltaX * PAGE_MODE_MULTIPLIER,\n y: e.deltaY * PAGE_MODE_MULTIPLIER,\n };\n }\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Common functions used both internally and externally, but which\n * must not be at the top level to avoid circular dependencies.\n *\n * @namespace Blockly.common\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.common');\n\n/* eslint-disable-next-line no-unused-vars */\nimport type {Block} from './block.js';\nimport {BlockDefinition, Blocks} from './blocks.js';\nimport type {Connection} from './connection.js';\nimport type {ICopyable} from './interfaces/i_copyable.js';\nimport type {Workspace} from './workspace.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\n/** Database of all workspaces. */\nconst WorkspaceDB_ = Object.create(null);\n\n\n/**\n * Find the workspace with the specified ID.\n *\n * @param id ID of workspace to find.\n * @returns The sought after workspace or null if not found.\n */\nexport function getWorkspaceById(id: string): Workspace|null {\n return WorkspaceDB_[id] || null;\n}\n\n/**\n * Find all workspaces.\n *\n * @returns Array of workspaces.\n */\nexport function getAllWorkspaces(): Workspace[] {\n const workspaces = [];\n for (const workspaceId in WorkspaceDB_) {\n workspaces.push(WorkspaceDB_[workspaceId]);\n }\n return workspaces;\n}\n\n/**\n * Register a workspace in the workspace db.\n *\n * @param workspace\n */\nexport function registerWorkspace(workspace: Workspace) {\n WorkspaceDB_[workspace.id] = workspace;\n}\n\n/**\n * Unregister a workspace from the workspace db.\n *\n * @param workspace\n */\nexport function unregisterWorkpace(workspace: Workspace) {\n delete WorkspaceDB_[workspace.id];\n}\n\n/**\n * The main workspace most recently used.\n * Set by Blockly.WorkspaceSvg.prototype.markFocused\n */\nlet mainWorkspace: Workspace;\n\n/**\n * Returns the last used top level workspace (based on focus). Try not to use\n * this function, particularly if there are multiple Blockly instances on a\n * page.\n *\n * @returns The main workspace.\n * @alias Blockly.common.getMainWorkspace\n */\nexport function getMainWorkspace(): Workspace {\n return mainWorkspace;\n}\n\n/**\n * Sets last used main workspace.\n *\n * @param workspace The most recently used top level workspace.\n * @alias Blockly.common.setMainWorkspace\n */\nexport function setMainWorkspace(workspace: Workspace) {\n mainWorkspace = workspace;\n}\n\n/**\n * Currently selected copyable object.\n */\nlet selected: ICopyable|null = null;\n\n/**\n * Returns the currently selected copyable object.\n *\n * @alias Blockly.common.getSelected\n */\nexport function getSelected(): ICopyable|null {\n return selected;\n}\n\n/**\n * Sets the currently selected block. This function does not visually mark the\n * block as selected or fire the required events. If you wish to\n * programmatically select a block, use `BlockSvg#select`.\n *\n * @param newSelection The newly selected block.\n * @alias Blockly.common.setSelected\n * @internal\n */\nexport function setSelected(newSelection: ICopyable|null) {\n selected = newSelection;\n}\n\n/**\n * Container element in which to render the WidgetDiv, DropDownDiv and Tooltip.\n */\nlet parentContainer: Element|null;\n\n/**\n * Get the container element in which to render the WidgetDiv, DropDownDiv and\n * Tooltip.\n *\n * @returns The parent container.\n * @alias Blockly.common.getParentContainer\n */\nexport function getParentContainer(): Element|null {\n return parentContainer;\n}\n\n/**\n * Set the parent container. This is the container element that the WidgetDiv,\n * DropDownDiv, and Tooltip are rendered into the first time `Blockly.inject`\n * is called.\n * This method is a NOP if called after the first `Blockly.inject`.\n *\n * @param newParent The container element.\n * @alias Blockly.common.setParentContainer\n */\nexport function setParentContainer(newParent: Element) {\n parentContainer = newParent;\n}\n\n/**\n * Size the SVG image to completely fill its container. Call this when the view\n * actually changes sizes (e.g. on a window resize/device orientation change).\n * See workspace.resizeContents to resize the workspace when the contents\n * change (e.g. when a block is added or removed).\n * Record the height/width of the SVG image.\n *\n * @param workspace Any workspace in the SVG.\n * @alias Blockly.common.svgResize\n */\nexport function svgResize(workspace: WorkspaceSvg) {\n let mainWorkspace = workspace;\n while (mainWorkspace.options.parentWorkspace) {\n mainWorkspace = mainWorkspace.options.parentWorkspace;\n }\n const svg = mainWorkspace.getParentSvg();\n const cachedSize = mainWorkspace.getCachedParentSvgSize();\n const div = svg.parentElement;\n if (!(div instanceof HTMLElement)) {\n // Workspace deleted, or something.\n return;\n }\n\n const width = div.offsetWidth;\n const height = div.offsetHeight;\n if (cachedSize.width !== width) {\n svg.setAttribute('width', width + 'px');\n mainWorkspace.setCachedParentSvgSize(width, null);\n }\n if (cachedSize.height !== height) {\n svg.setAttribute('height', height + 'px');\n mainWorkspace.setCachedParentSvgSize(null, height);\n }\n mainWorkspace.resize();\n}\n\n/**\n * All of the connections on blocks that are currently being dragged.\n */\nexport const draggingConnections: Connection[] = [];\n\n/**\n * Get a map of all the block's descendants mapping their type to the number of\n * children with that type.\n *\n * @param block The block to map.\n * @param opt_stripFollowing Optionally ignore all following\n * statements (blocks that are not inside a value or statement input\n * of the block).\n * @returns Map of types to type counts for descendants of the bock.\n * @alias Blockly.common.getBlockTypeCounts\n */\nexport function getBlockTypeCounts(\n block: Block, opt_stripFollowing?: boolean): {[key: string]: number} {\n const typeCountsMap = Object.create(null);\n const descendants = block.getDescendants(true);\n if (opt_stripFollowing) {\n const nextBlock = block.getNextBlock();\n if (nextBlock) {\n const index = descendants.indexOf(nextBlock);\n descendants.splice(index, descendants.length - index);\n }\n }\n for (let i = 0, checkBlock; checkBlock = descendants[i]; i++) {\n if (typeCountsMap[checkBlock.type]) {\n typeCountsMap[checkBlock.type]++;\n } else {\n typeCountsMap[checkBlock.type] = 1;\n }\n }\n return typeCountsMap;\n}\n\n/**\n * Helper function for defining a block from JSON. The resulting function has\n * the correct value of jsonDef at the point in code where jsonInit is called.\n *\n * @param jsonDef The JSON definition of a block.\n * @returns A function that calls jsonInit with the correct value\n * of jsonDef.\n */\nfunction jsonInitFactory(jsonDef: AnyDuringMigration): () => void {\n return function(this: Block) {\n this.jsonInit(jsonDef);\n };\n}\n\n/**\n * Define blocks from an array of JSON block definitions, as might be generated\n * by the Blockly Developer Tools.\n *\n * @param jsonArray An array of JSON block definitions.\n * @alias Blockly.common.defineBlocksWithJsonArray\n */\nexport function defineBlocksWithJsonArray(jsonArray: AnyDuringMigration[]) {\n TEST_ONLY.defineBlocksWithJsonArrayInternal(jsonArray);\n}\n\n/**\n * Private version of defineBlocksWithJsonArray for stubbing in tests.\n */\nfunction defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) {\n defineBlocks(createBlockDefinitionsFromJsonArray(jsonArray));\n}\n\n/**\n * Define blocks from an array of JSON block definitions, as might be generated\n * by the Blockly Developer Tools.\n *\n * @param jsonArray An array of JSON block definitions.\n * @returns A map of the block\n * definitions created.\n * @alias Blockly.common.defineBlocksWithJsonArray\n */\nexport function createBlockDefinitionsFromJsonArray(\n jsonArray: AnyDuringMigration[]): {[key: string]: BlockDefinition} {\n const blocks: {[key: string]: BlockDefinition} = {};\n for (let i = 0; i < jsonArray.length; i++) {\n const elem = jsonArray[i];\n if (!elem) {\n console.warn(`Block definition #${i} in JSON array is ${elem}. Skipping`);\n continue;\n }\n const type = elem['type'];\n if (!type) {\n console.warn(\n `Block definition #${i} in JSON array is missing a type attribute. ` +\n 'Skipping.');\n continue;\n }\n blocks[type] = {init: jsonInitFactory(elem)};\n }\n return blocks;\n}\n\n/**\n * Add the specified block definitions to the block definitions\n * dictionary (Blockly.Blocks).\n *\n * @param blocks A map of block\n * type names to block definitions.\n * @alias Blockly.common.defineBlocks\n */\nexport function defineBlocks(blocks: {[key: string]: BlockDefinition}) {\n // Iterate over own enumerable properties.\n for (const type of Object.keys(blocks)) {\n const definition = blocks[type];\n if (type in Blocks) {\n console.warn(`Block definiton \"${type}\" overwrites previous definition.`);\n }\n Blocks[type] = definition;\n }\n}\n\nexport const TEST_ONLY = {defineBlocksWithJsonArrayInternal};\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods for DOM manipulation.\n * These methods are not specific to Blockly, and could be factored out into\n * a JavaScript framework such as Closure.\n *\n * @namespace Blockly.utils.dom\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.dom');\n\nimport type {Svg} from './svg.js';\n\n\n/**\n * Required name space for SVG elements.\n *\n * @alias Blockly.utils.dom.SVG_NS\n */\nexport const SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Required name space for HTML elements.\n *\n * @alias Blockly.utils.dom.HTML_NS\n */\nexport const HTML_NS = 'http://www.w3.org/1999/xhtml';\n\n/**\n * Required name space for XLINK elements.\n *\n * @alias Blockly.utils.dom.XLINK_NS\n */\nexport const XLINK_NS = 'http://www.w3.org/1999/xlink';\n\n/**\n * Node type constants.\n * https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\n *\n * @alias Blockly.utils.dom.NodeType\n */\nexport enum NodeType {\n ELEMENT_NODE = 1,\n TEXT_NODE = 3,\n COMMENT_NODE = 8,\n DOCUMENT_POSITION_CONTAINED_BY = 16\n}\n\n/** Temporary cache of text widths. */\nlet cacheWidths: {[key: string]: number}|null = null;\n\n/** Number of current references to cache. */\nlet cacheReference = 0;\n\n/** A HTML canvas context used for computing text width. */\nlet canvasContext: CanvasRenderingContext2D|null = null;\n\n/**\n * Helper method for creating SVG elements.\n *\n * @param name Element's tag name.\n * @param attrs Dictionary of attribute names and values.\n * @param opt_parent Optional parent on which to append the element.\n * @returns if name is a string or a more specific type if it a member of Svg.\n * @alias Blockly.utils.dom.createSvgElement\n */\nexport function createSvgElement(\n name: string|Svg, attrs: {[key: string]: string|number},\n opt_parent?: Element|null): T {\n const e = document.createElementNS(SVG_NS, String(name)) as T;\n for (const key in attrs) {\n e.setAttribute(key, `${attrs[key]}`);\n }\n if (opt_parent) {\n opt_parent.appendChild(e);\n }\n return e;\n}\n\n/**\n * Add a CSS class to a element.\n *\n * Handles multiple space-separated classes for legacy reasons.\n *\n * @param element DOM element to add class to.\n * @param className Name of class to add.\n * @returns True if class was added, false if already present.\n * @alias Blockly.utils.dom.addClass\n */\nexport function addClass(element: Element, className: string): boolean {\n const classNames = className.split(' ');\n if (classNames.every((name) => element.classList.contains(name))) {\n return false;\n }\n element.classList.add(...classNames);\n return true;\n}\n\n/**\n * Removes multiple classes from an element.\n *\n * @param element DOM element to remove classes from.\n * @param classNames A string of one or multiple class names for an element.\n * @alias Blockly.utils.dom.removeClasses\n */\nexport function removeClasses(element: Element, classNames: string) {\n element.classList.remove(...classNames.split(' '));\n}\n\n/**\n * Remove a CSS class from a element.\n *\n * Handles multiple space-separated classes for legacy reasons.\n *\n * @param element DOM element to remove class from.\n * @param className Name of class to remove.\n * @returns True if class was removed, false if never present.\n * @alias Blockly.utils.dom.removeClass\n */\nexport function removeClass(element: Element, className: string): boolean {\n const classNames = className.split(' ');\n if (classNames.every((name) => !element.classList.contains(name))) {\n return false;\n }\n element.classList.remove(...classNames);\n return true;\n}\n\n/**\n * Checks if an element has the specified CSS class.\n *\n * @param element DOM element to check.\n * @param className Name of class to check.\n * @returns True if class exists, false otherwise.\n * @alias Blockly.utils.dom.hasClass\n */\nexport function hasClass(element: Element, className: string): boolean {\n return element.classList.contains(className);\n}\n\n/**\n * Removes a node from its parent. No-op if not attached to a parent.\n *\n * @param node The node to remove.\n * @returns The node removed if removed; else, null.\n * @alias Blockly.utils.dom.removeNode\n */\n// Copied from Closure goog.dom.removeNode\nexport function removeNode(node: Node|null): Node|null {\n return node && node.parentNode ? node.parentNode.removeChild(node) : null;\n}\n\n/**\n * Insert a node after a reference node.\n * Contrast with node.insertBefore function.\n *\n * @param newNode New element to insert.\n * @param refNode Existing element to precede new node.\n * @alias Blockly.utils.dom.insertAfter\n */\nexport function insertAfter(newNode: Element, refNode: Element) {\n const siblingNode = refNode.nextSibling;\n const parentNode = refNode.parentNode;\n if (!parentNode) {\n throw Error('Reference node has no parent.');\n }\n if (siblingNode) {\n parentNode.insertBefore(newNode, siblingNode);\n } else {\n parentNode.appendChild(newNode);\n }\n}\n\n/**\n * Whether a node contains another node.\n *\n * @param parent The node that should contain the other node.\n * @param descendant The node to test presence of.\n * @returns Whether the parent node contains the descendant node.\n * @alias Blockly.utils.dom.containsNode\n */\nexport function containsNode(parent: Node, descendant: Node): boolean {\n return !!(\n parent.compareDocumentPosition(descendant) &\n NodeType.DOCUMENT_POSITION_CONTAINED_BY);\n}\n\n/**\n * Sets the CSS transform property on an element. This function sets the\n * non-vendor-prefixed and vendor-prefixed versions for backwards compatibility\n * with older browsers. See https://caniuse.com/#feat=transforms2d\n *\n * @param element Element to which the CSS transform will be applied.\n * @param transform The value of the CSS `transform` property.\n * @alias Blockly.utils.dom.setCssTransform\n */\nexport function setCssTransform(\n element: HTMLElement|SVGElement, transform: string) {\n element.style['transform'] = transform;\n element.style['-webkit-transform' as any] = transform;\n}\n\n/**\n * Start caching text widths. Every call to this function MUST also call\n * stopTextWidthCache. Caches must not survive between execution threads.\n *\n * @alias Blockly.utils.dom.startTextWidthCache\n */\nexport function startTextWidthCache() {\n cacheReference++;\n if (!cacheWidths) {\n cacheWidths = Object.create(null);\n }\n}\n\n/**\n * Stop caching field widths. Unless caching was already on when the\n * corresponding call to startTextWidthCache was made.\n *\n * @alias Blockly.utils.dom.stopTextWidthCache\n */\nexport function stopTextWidthCache() {\n cacheReference--;\n if (!cacheReference) {\n cacheWidths = null;\n }\n}\n\n/**\n * Gets the width of a text element, caching it in the process.\n *\n * @param textElement An SVG 'text' element.\n * @returns Width of element.\n * @alias Blockly.utils.dom.getTextWidth\n */\nexport function getTextWidth(textElement: SVGTextElement): number {\n const key = textElement.textContent + '\\n' + textElement.className.baseVal;\n let width;\n // Return the cached width if it exists.\n if (cacheWidths) {\n width = cacheWidths[key];\n if (width) {\n return width;\n }\n }\n\n // Attempt to compute fetch the width of the SVG text element.\n try {\n width = textElement.getComputedTextLength();\n } catch (e) {\n // In other cases where we fail to get the computed text. Instead, use an\n // approximation and do not cache the result. At some later point in time\n // when the block is inserted into the visible DOM, this method will be\n // called again and, at that point in time, will not throw an exception.\n return textElement.textContent!.length * 8;\n }\n\n // Cache the computed width and return.\n if (cacheWidths) {\n cacheWidths[key] = width;\n }\n return width;\n}\n\n/**\n * Gets the width of a text element using a faster method than `getTextWidth`.\n * This method requires that we know the text element's font family and size in\n * advance. Similar to `getTextWidth`, we cache the width we compute.\n *\n * @param textElement An SVG 'text' element.\n * @param fontSize The font size to use.\n * @param fontWeight The font weight to use.\n * @param fontFamily The font family to use.\n * @returns Width of element.\n * @alias Blockly.utils.dom.getFastTextWidth\n */\nexport function getFastTextWidth(\n textElement: SVGTextElement, fontSize: number, fontWeight: string,\n fontFamily: string): number {\n return getFastTextWidthWithSizeString(\n textElement, fontSize + 'pt', fontWeight, fontFamily);\n}\n\n/**\n * Gets the width of a text element using a faster method than `getTextWidth`.\n * This method requires that we know the text element's font family and size in\n * advance. Similar to `getTextWidth`, we cache the width we compute.\n * This method is similar to `getFastTextWidth` but expects the font size\n * parameter to be a string.\n *\n * @param textElement An SVG 'text' element.\n * @param fontSize The font size to use.\n * @param fontWeight The font weight to use.\n * @param fontFamily The font family to use.\n * @returns Width of element.\n * @alias Blockly.utils.dom.getFastTextWidthWithSizeString\n */\nexport function getFastTextWidthWithSizeString(\n textElement: SVGTextElement, fontSize: string, fontWeight: string,\n fontFamily: string): number {\n const text = textElement.textContent;\n const key = text + '\\n' + textElement.className.baseVal;\n let width;\n\n // Return the cached width if it exists.\n if (cacheWidths) {\n width = cacheWidths[key];\n if (width) {\n return width;\n }\n }\n\n if (!canvasContext) {\n // Inject the canvas element used for computing text widths.\n const computeCanvas = (document.createElement('canvas'));\n computeCanvas.className = 'blocklyComputeCanvas';\n document.body.appendChild(computeCanvas);\n\n // Initialize the HTML canvas context and set the font.\n // The context font must match blocklyText's fontsize and font-family\n // set in CSS.\n canvasContext = computeCanvas.getContext('2d') as CanvasRenderingContext2D;\n }\n // Set the desired font size and family.\n canvasContext.font = fontWeight + ' ' + fontSize + ' ' + fontFamily;\n\n // Measure the text width using the helper canvas context.\n if (text) {\n width = canvasContext.measureText(text).width;\n } else {\n width = 0;\n }\n\n // Cache the computed width and return.\n if (cacheWidths) {\n cacheWidths[key] = width;\n }\n return width;\n}\n\n/**\n * Measure a font's metrics. The height and baseline values.\n *\n * @param text Text to measure the font dimensions of.\n * @param fontSize The font size to use.\n * @param fontWeight The font weight to use.\n * @param fontFamily The font family to use.\n * @returns Font measurements.\n * @alias Blockly.utils.dom.measureFontMetrics\n */\nexport function measureFontMetrics(\n text: string, fontSize: string, fontWeight: string,\n fontFamily: string): {height: number, baseline: number} {\n const span = (document.createElement('span'));\n span.style.font = fontWeight + ' ' + fontSize + ' ' + fontFamily;\n span.textContent = text;\n\n const block = (document.createElement('div'));\n block.style.width = '1px';\n block.style.height = '0';\n\n const div = (document.createElement('div'));\n div.setAttribute('style', 'position: fixed; top: 0; left: 0; display: flex;');\n div.appendChild(span);\n div.appendChild(block);\n\n document.body.appendChild(div);\n const result = {\n height: 0,\n baseline: 0,\n };\n try {\n div.style.alignItems = 'baseline';\n result.baseline = block.offsetTop - span.offsetTop;\n div.style.alignItems = 'flex-end';\n result.height = block.offsetTop - span.offsetTop;\n } finally {\n document.body.removeChild(div);\n }\n return result;\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods for math.\n * These methods are not specific to Blockly, and could be factored out into\n * a JavaScript framework such as Closure.\n *\n * @namespace Blockly.utils.math\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.math');\n\n\n/**\n * Converts degrees to radians.\n * Copied from Closure's goog.math.toRadians.\n *\n * @param angleDegrees Angle in degrees.\n * @returns Angle in radians.\n * @alias Blockly.utils.math.toRadians\n */\nexport function toRadians(angleDegrees: number): number {\n return angleDegrees * Math.PI / 180;\n}\n\n/**\n * Converts radians to degrees.\n * Copied from Closure's goog.math.toDegrees.\n *\n * @param angleRadians Angle in radians.\n * @returns Angle in degrees.\n * @alias Blockly.utils.math.toDegrees\n */\nexport function toDegrees(angleRadians: number): number {\n return angleRadians * 180 / Math.PI;\n}\n\n/**\n * Clamp the provided number between the lower bound and the upper bound.\n *\n * @param lowerBound The desired lower bound.\n * @param number The number to clamp.\n * @param upperBound The desired upper bound.\n * @returns The clamped number.\n * @alias Blockly.utils.math.clamp\n */\nexport function clamp(\n lowerBound: number, number: number, upperBound: number): number {\n if (upperBound < lowerBound) {\n const temp = upperBound;\n upperBound = lowerBound;\n lowerBound = temp;\n }\n return Math.max(lowerBound, Math.min(number, upperBound));\n}\n","/**\n * @license\n * Copyright 2020 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Helper function for warning developers about deprecations.\n * This method is not specific to Blockly.\n *\n * @namespace Blockly.utils.deprecation\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.deprecation');\n\n\n/**\n * Warn developers that a function or property is deprecated.\n *\n * @param name The name of the function or property.\n * @param deprecationDate The date of deprecation. Prefer 'version n.0.0'\n * format, and fall back to 'month yyyy' or 'quarter yyyy' format.\n * @param deletionDate The date of deletion. Prefer 'version n.0.0'\n * format, and fall back to 'month yyyy' or 'quarter yyyy' format.\n * @param opt_use The name of a function or property to use instead, if any.\n * @alias Blockly.utils.deprecation.warn\n * @internal\n */\nexport function warn(\n name: string, deprecationDate: string, deletionDate: string,\n opt_use?: string) {\n let msg = name + ' was deprecated in ' + deprecationDate +\n ' and will be deleted in ' + deletionDate + '.';\n if (opt_use) {\n msg += '\\nUse ' + opt_use + ' instead.';\n }\n console.warn(msg);\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utilities for element styles.\n * These methods are not specific to Blockly, and could be factored out into\n * a JavaScript framework such as Closure.\n *\n * @namespace Blockly.utils.style\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.style');\n\nimport * as deprecation from './deprecation.js';\nimport {Coordinate} from './coordinate.js';\nimport {Rect} from './rect.js';\nimport {Size} from './size.js';\n\n\n/**\n * Gets the height and width of an element.\n * Similar to Closure's goog.style.getSize\n *\n * @param element Element to get size of.\n * @returns Object with width/height properties.\n * @alias Blockly.utils.style.getSize\n */\nexport function getSize(element: Element): Size {\n return TEST_ONLY.getSizeInternal(element);\n}\n\n/**\n * Private version of getSize for stubbing in tests.\n */\nfunction getSizeInternal(element: Element): Size {\n if (getComputedStyle(element, 'display') !== 'none') {\n return getSizeWithDisplay(element);\n }\n\n // Evaluate size with a temporary element.\n // AnyDuringMigration because: Property 'style' does not exist on type\n // 'Element'.\n const style = (element as AnyDuringMigration).style;\n const originalDisplay = style.display;\n const originalVisibility = style.visibility;\n const originalPosition = style.position;\n\n style.visibility = 'hidden';\n style.position = 'absolute';\n style.display = 'inline';\n\n const offsetWidth = (element as HTMLElement).offsetWidth;\n const offsetHeight = (element as HTMLElement).offsetHeight;\n\n style.display = originalDisplay;\n style.position = originalPosition;\n style.visibility = originalVisibility;\n\n return new Size(offsetWidth, offsetHeight);\n}\n\n/**\n * Gets the height and width of an element when the display is not none.\n *\n * @param element Element to get size of.\n * @returns Object with width/height properties.\n */\nfunction getSizeWithDisplay(element: Element): Size {\n const offsetWidth = (element as HTMLElement).offsetWidth;\n const offsetHeight = (element as HTMLElement).offsetHeight;\n return new Size(offsetWidth, offsetHeight);\n}\n\n/**\n * Retrieves a computed style value of a node. It returns empty string\n * if the property requested is an SVG one and it has not been\n * explicitly set (firefox and webkit).\n *\n * Copied from Closure's goog.style.getComputedStyle\n *\n * @param element Element to get style of.\n * @param property Property to get (camel-case).\n * @returns Style value.\n * @alias Blockly.utils.style.getComputedStyle\n */\nexport function getComputedStyle(element: Element, property: string): string {\n const styles = window.getComputedStyle(element);\n // element.style[..] is undefined for browser specific styles\n // as 'filter'.\n return (styles as AnyDuringMigration)[property] ||\n styles.getPropertyValue(property);\n}\n\n/**\n * Gets the cascaded style value of a node, or null if the value cannot be\n * computed (only Internet Explorer can do this).\n *\n * Copied from Closure's goog.style.getCascadedStyle\n *\n * @param element Element to get style of.\n * @param style Property to get (camel-case).\n * @returns Style value.\n * @deprecated No longer provided by Blockly.\n * @alias Blockly.utils.style.getCascadedStyle\n */\nexport function getCascadedStyle(element: Element, style: string): string {\n deprecation.warn(\n 'Blockly.utils.style.getCascadedStyle', 'version 9', 'version 10');\n // AnyDuringMigration because: Property 'currentStyle' does not exist on type\n // 'Element'. AnyDuringMigration because: Property 'currentStyle' does not\n // exist on type 'Element'.\n return (element as AnyDuringMigration).currentStyle ?\n (element as AnyDuringMigration).currentStyle[style] :\n '' as string;\n}\n\n/**\n * Returns a Coordinate object relative to the top-left of the HTML document.\n * Similar to Closure's goog.style.getPageOffset\n *\n * @param el Element to get the page offset for.\n * @returns The page offset.\n * @alias Blockly.utils.style.getPageOffset\n */\nexport function getPageOffset(el: Element): Coordinate {\n const pos = new Coordinate(0, 0);\n const box = el.getBoundingClientRect();\n const documentElement = document.documentElement;\n // Must add the scroll coordinates in to get the absolute page offset\n // of element since getBoundingClientRect returns relative coordinates to\n // the viewport.\n const scrollCoord = new Coordinate(\n window.pageXOffset || documentElement.scrollLeft,\n window.pageYOffset || documentElement.scrollTop);\n pos.x = box.left + scrollCoord.x;\n pos.y = box.top + scrollCoord.y;\n\n return pos;\n}\n\n/**\n * Calculates the viewport coordinates relative to the document.\n * Similar to Closure's goog.style.getViewportPageOffset\n *\n * @returns The page offset of the viewport.\n * @alias Blockly.utils.style.getViewportPageOffset\n */\nexport function getViewportPageOffset(): Coordinate {\n const body = document.body;\n const documentElement = document.documentElement;\n const scrollLeft = body.scrollLeft || documentElement.scrollLeft;\n const scrollTop = body.scrollTop || documentElement.scrollTop;\n return new Coordinate(scrollLeft, scrollTop);\n}\n\n/**\n * Gets the computed border widths (on all sides) in pixels\n * Copied from Closure's goog.style.getBorderBox\n *\n * @param element The element to get the border widths for.\n * @returns The computed border widths.\n * @alias Blockly.utils.style.getBorderBox\n */\nexport function getBorderBox(element: Element): Rect {\n const left = parseFloat(getComputedStyle(element, 'borderLeftWidth'));\n const right = parseFloat(getComputedStyle(element, 'borderRightWidth'));\n const top = parseFloat(getComputedStyle(element, 'borderTopWidth'));\n const bottom = parseFloat(getComputedStyle(element, 'borderBottomWidth'));\n\n return new Rect(top, bottom, left, right);\n}\n\n/**\n * Changes the scroll position of `container` with the minimum amount so\n * that the content and the borders of the given `element` become visible.\n * If the element is bigger than the container, its top left corner will be\n * aligned as close to the container's top left corner as possible.\n * Copied from Closure's goog.style.scrollIntoContainerView\n *\n * @param element The element to make visible.\n * @param container The container to scroll. If not set, then the document\n * scroll element will be used.\n * @param opt_center Whether to center the element in the container.\n * Defaults to false.\n * @alias Blockly.utils.style.scrollIntoContainerView\n */\nexport function scrollIntoContainerView(\n element: Element, container: Element, opt_center?: boolean) {\n const offset = getContainerOffsetToScrollInto(element, container, opt_center);\n container.scrollLeft = offset.x;\n container.scrollTop = offset.y;\n}\n\n/**\n * Calculate the scroll position of `container` with the minimum amount so\n * that the content and the borders of the given `element` become visible.\n * If the element is bigger than the container, its top left corner will be\n * aligned as close to the container's top left corner as possible.\n * Copied from Closure's goog.style.getContainerOffsetToScrollInto\n *\n * @param element The element to make visible.\n * @param container The container to scroll. If not set, then the document\n * scroll element will be used.\n * @param opt_center Whether to center the element in the container.\n * Defaults to false.\n * @returns The new scroll position of the container.\n * @alias Blockly.utils.style.getContainerOffsetToScrollInto\n */\nexport function getContainerOffsetToScrollInto(\n element: Element, container: Element, opt_center?: boolean): Coordinate {\n // Absolute position of the element's border's top left corner.\n const elementPos = getPageOffset(element);\n // Absolute position of the container's border's top left corner.\n const containerPos = getPageOffset(container);\n const containerBorder = getBorderBox(container);\n // Relative pos. of the element's border box to the container's content box.\n const relX = elementPos.x - containerPos.x - containerBorder.left;\n const relY = elementPos.y - containerPos.y - containerBorder.top;\n // How much the element can move in the container, i.e. the difference between\n // the element's bottom-right-most and top-left-most position where it's\n // fully visible.\n const elementSize = getSizeWithDisplay(element);\n const spaceX = container.clientWidth - elementSize.width;\n const spaceY = container.clientHeight - elementSize.height;\n let scrollLeft = container.scrollLeft;\n let scrollTop = container.scrollTop;\n if (opt_center) {\n // All browsers round non-integer scroll positions down.\n scrollLeft += relX - spaceX / 2;\n scrollTop += relY - spaceY / 2;\n } else {\n // This formula was designed to give the correct scroll values in the\n // following cases:\n // - element is higher than container (spaceY < 0) => scroll down by relY\n // - element is not higher that container (spaceY >= 0):\n // - it is above container (relY < 0) => scroll up by abs(relY)\n // - it is below container (relY > spaceY) => scroll down by relY - spaceY\n // - it is in the container => don't scroll\n scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));\n scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));\n }\n return new Coordinate(scrollLeft, scrollTop);\n}\n\nexport const TEST_ONLY = {\n getSizeInternal,\n};\n","/**\n * @license\n * Copyright 2016 Massachusetts Institute of Technology\n * All rights reserved.\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * A div that floats on top of the workspace, for drop-down menus.\n *\n * @class\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.dropDownDiv');\n\nimport type {BlockSvg} from './block_svg.js';\nimport * as common from './common.js';\nimport * as dom from './utils/dom.js';\nimport type {Field} from './field.js';\nimport * as math from './utils/math.js';\nimport {Rect} from './utils/rect.js';\nimport type {Size} from './utils/size.js';\nimport * as style from './utils/style.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\n/**\n * Arrow size in px. Should match the value in CSS\n * (need to position pre-render).\n */\nexport const ARROW_SIZE = 16;\n\n/**\n * Drop-down border size in px. Should match the value in CSS (need to position\n * the arrow).\n */\nexport const BORDER_SIZE = 1;\n\n/**\n * Amount the arrow must be kept away from the edges of the main drop-down div,\n * in px.\n */\nexport const ARROW_HORIZONTAL_PADDING = 12;\n\n/** Amount drop-downs should be padded away from the source, in px. */\nexport const PADDING_Y = 16;\n\n/** Length of animations in seconds. */\nexport const ANIMATION_TIME = 0.25;\n\n/**\n * Timer for animation out, to be cleared if we need to immediately hide\n * without disrupting new shows.\n */\nlet animateOutTimer: ReturnType|null = null;\n\n/** Callback for when the drop-down is hidden. */\nlet onHide: Function|null = null;\n\n/** A class name representing the current owner's workspace renderer. */\nlet renderedClassName = '';\n\n/** A class name representing the current owner's workspace theme. */\nlet themeClassName = '';\n\n/** The content element. */\nlet div: HTMLDivElement;\n\n/** The content element. */\nlet content: HTMLDivElement;\n\n/** The arrow element. */\nlet arrow: HTMLDivElement;\n\n/**\n * Drop-downs will appear within the bounds of this element if possible.\n * Set in setBoundsElement.\n */\nlet boundsElement: Element|null = null;\n\n/** The object currently using the drop-down. */\nlet owner: Field|null = null;\n\n/** Whether the dropdown was positioned to a field or the source block. */\nlet positionToField: boolean|null = null;\n\n/**\n * Dropdown bounds info object used to encapsulate sizing information about a\n * bounding element (bounding box and width/height).\n */\nexport interface BoundsInfo {\n top: number;\n left: number;\n bottom: number;\n right: number;\n width: number;\n height: number;\n}\n\n/** Dropdown position metrics. */\nexport interface PositionMetrics {\n initialX: number;\n initialY: number;\n finalX: number;\n finalY: number;\n arrowX: number|null;\n arrowY: number|null;\n arrowAtTop: boolean|null;\n arrowVisible: boolean;\n}\n\n/**\n * Create and insert the DOM element for this div.\n *\n * @internal\n */\nexport function createDom() {\n if (div) {\n return; // Already created.\n }\n div = document.createElement('div');\n div.className = 'blocklyDropDownDiv';\n const parentDiv = common.getParentContainer() || document.body;\n parentDiv.appendChild(div);\n\n content = document.createElement('div');\n content.className = 'blocklyDropDownContent';\n div.appendChild(content);\n\n arrow = document.createElement('div');\n arrow.className = 'blocklyDropDownArrow';\n div.appendChild(arrow);\n\n div.style.opacity = '0';\n // Transition animation for transform: translate() and opacity.\n div.style.transition = 'transform ' + ANIMATION_TIME + 's, ' +\n 'opacity ' + ANIMATION_TIME + 's';\n\n // Handle focusin/out events to add a visual indicator when\n // a child is focused or blurred.\n div.addEventListener('focusin', function() {\n dom.addClass(div, 'blocklyFocused');\n });\n div.addEventListener('focusout', function() {\n dom.removeClass(div, 'blocklyFocused');\n });\n}\n\n/**\n * Set an element to maintain bounds within. Drop-downs will appear\n * within the box of this element if possible.\n *\n * @param boundsElem Element to bind drop-down to.\n */\nexport function setBoundsElement(boundsElem: Element|null) {\n boundsElement = boundsElem;\n}\n\n/**\n * Provide the div for inserting content into the drop-down.\n *\n * @returns Div to populate with content.\n */\nexport function getContentDiv(): Element {\n return content;\n}\n\n/** Clear the content of the drop-down. */\nexport function clearContent() {\n content.textContent = '';\n content.style.width = '';\n}\n\n/**\n * Set the colour for the drop-down.\n *\n * @param backgroundColour Any CSS colour for the background.\n * @param borderColour Any CSS colour for the border.\n */\nexport function setColour(backgroundColour: string, borderColour: string) {\n div.style.backgroundColor = backgroundColour;\n div.style.borderColor = borderColour;\n}\n\n/**\n * Shortcut to show and place the drop-down with positioning determined\n * by a particular block. The primary position will be below the block,\n * and the secondary position above the block. Drop-down will be\n * constrained to the block's workspace.\n *\n * @param field The field showing the drop-down.\n * @param block Block to position the drop-down around.\n * @param opt_onHide Optional callback for when the drop-down is hidden.\n * @param opt_secondaryYOffset Optional Y offset for above-block positioning.\n * @returns True if the menu rendered below block; false if above.\n */\nexport function showPositionedByBlock(\n field: Field, block: BlockSvg, opt_onHide?: Function,\n opt_secondaryYOffset?: number): boolean {\n return showPositionedByRect(\n getScaledBboxOfBlock(block), field, opt_onHide, opt_secondaryYOffset);\n}\n\n/**\n * Shortcut to show and place the drop-down with positioning determined\n * by a particular field. The primary position will be below the field,\n * and the secondary position above the field. Drop-down will be\n * constrained to the block's workspace.\n *\n * @param field The field to position the dropdown against.\n * @param opt_onHide Optional callback for when the drop-down is hidden.\n * @param opt_secondaryYOffset Optional Y offset for above-block positioning.\n * @returns True if the menu rendered below block; false if above.\n */\nexport function showPositionedByField(\n field: Field, opt_onHide?: Function,\n opt_secondaryYOffset?: number): boolean {\n positionToField = true;\n return showPositionedByRect(\n getScaledBboxOfField(field), field, opt_onHide, opt_secondaryYOffset);\n}\n/**\n * Get the scaled bounding box of a block.\n *\n * @param block The block.\n * @returns The scaled bounding box of the block.\n */\nfunction getScaledBboxOfBlock(block: BlockSvg): Rect {\n const blockSvg = block.getSvgRoot();\n const scale = block.workspace.scale;\n const scaledHeight = block.height * scale;\n const scaledWidth = block.width * scale;\n const xy = style.getPageOffset(blockSvg);\n return new Rect(xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth);\n}\n\n/**\n * Get the scaled bounding box of a field.\n *\n * @param field The field.\n * @returns The scaled bounding box of the field.\n */\nfunction getScaledBboxOfField(field: Field): Rect {\n const bBox = field.getScaledBBox();\n return new Rect(bBox.top, bBox.bottom, bBox.left, bBox.right);\n}\n\n/**\n * Helper method to show and place the drop-down with positioning determined\n * by a scaled bounding box. The primary position will be below the rect,\n * and the secondary position above the rect. Drop-down will be constrained to\n * the block's workspace.\n *\n * @param bBox The scaled bounding box.\n * @param field The field to position the dropdown against.\n * @param opt_onHide Optional callback for when the drop-down is hidden.\n * @param opt_secondaryYOffset Optional Y offset for above-block positioning.\n * @returns True if the menu rendered below block; false if above.\n */\nfunction showPositionedByRect(\n bBox: Rect, field: Field, opt_onHide?: Function,\n opt_secondaryYOffset?: number): boolean {\n // If we can fit it, render below the block.\n const primaryX = bBox.left + (bBox.right - bBox.left) / 2;\n const primaryY = bBox.bottom;\n // If we can't fit it, render above the entire parent block.\n const secondaryX = primaryX;\n let secondaryY = bBox.top;\n if (opt_secondaryYOffset) {\n secondaryY += opt_secondaryYOffset;\n }\n const sourceBlock = field.getSourceBlock() as BlockSvg;\n // Set bounds to main workspace; show the drop-down.\n let workspace = sourceBlock.workspace;\n while (workspace.options.parentWorkspace) {\n workspace = workspace.options.parentWorkspace;\n }\n setBoundsElement(workspace.getParentSvg().parentNode as Element | null);\n return show(\n field, sourceBlock.RTL, primaryX, primaryY, secondaryX, secondaryY,\n opt_onHide);\n}\n\n/**\n * Show and place the drop-down.\n * The drop-down is placed with an absolute \"origin point\" (x, y) - i.e.,\n * the arrow will point at this origin and box will positioned below or above\n * it. If we can maintain the container bounds at the primary point, the arrow\n * will point there, and the container will be positioned below it.\n * If we can't maintain the container bounds at the primary point, fall-back to\n * the secondary point and position above.\n *\n * @param newOwner The object showing the drop-down\n * @param rtl Right-to-left (true) or left-to-right (false).\n * @param primaryX Desired origin point x, in absolute px.\n * @param primaryY Desired origin point y, in absolute px.\n * @param secondaryX Secondary/alternative origin point x, in absolute px.\n * @param secondaryY Secondary/alternative origin point y, in absolute px.\n * @param opt_onHide Optional callback for when the drop-down is hidden.\n * @returns True if the menu rendered at the primary origin point.\n * @internal\n */\nexport function show(\n newOwner: Field, rtl: boolean, primaryX: number, primaryY: number,\n secondaryX: number, secondaryY: number, opt_onHide?: Function): boolean {\n owner = newOwner;\n onHide = opt_onHide || null;\n // Set direction.\n div.style.direction = rtl ? 'rtl' : 'ltr';\n\n const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;\n renderedClassName = mainWorkspace.getRenderer().getClassName();\n themeClassName = mainWorkspace.getTheme().getClassName();\n if (renderedClassName) {\n dom.addClass(div, renderedClassName);\n }\n if (themeClassName) {\n dom.addClass(div, themeClassName);\n }\n\n // When we change `translate` multiple times in close succession,\n // Chrome may choose to wait and apply them all at once.\n // Since we want the translation to initial X, Y to be immediate,\n // and the translation to final X, Y to be animated,\n // we saw problems where both would be applied after animation was turned on,\n // making the dropdown appear to fly in from (0, 0).\n // Using both `left`, `top` for the initial translation and then `translate`\n // for the animated transition to final X, Y is a workaround.\n return positionInternal(primaryX, primaryY, secondaryX, secondaryY);\n}\n\nconst internal = {\n /**\n * Get sizing info about the bounding element.\n *\n * @returns An object containing size information about the bounding element\n * (bounding box and width/height).\n */\n getBoundsInfo: function(): BoundsInfo {\n const boundPosition = style.getPageOffset(boundsElement as Element);\n const boundSize = style.getSize(boundsElement as Element);\n\n return {\n left: boundPosition.x,\n right: boundPosition.x + boundSize.width,\n top: boundPosition.y,\n bottom: boundPosition.y + boundSize.height,\n width: boundSize.width,\n height: boundSize.height,\n };\n },\n\n /**\n * Helper to position the drop-down and the arrow, maintaining bounds.\n * See explanation of origin points in show.\n *\n * @param primaryX Desired origin point x, in absolute px.\n * @param primaryY Desired origin point y, in absolute px.\n * @param secondaryX Secondary/alternative origin point x, in absolute px.\n * @param secondaryY Secondary/alternative origin point y, in absolute px.\n * @returns Various final metrics, including rendered positions for drop-down\n * and arrow.\n */\n getPositionMetrics: function(\n primaryX: number, primaryY: number, secondaryX: number,\n secondaryY: number): PositionMetrics {\n const boundsInfo = internal.getBoundsInfo();\n const divSize = style.getSize(div as Element);\n\n // Can we fit in-bounds below the target?\n if (primaryY + divSize.height < boundsInfo.bottom) {\n return getPositionBelowMetrics(primaryX, primaryY, boundsInfo, divSize);\n }\n // Can we fit in-bounds above the target?\n if (secondaryY - divSize.height > boundsInfo.top) {\n return getPositionAboveMetrics(\n secondaryX, secondaryY, boundsInfo, divSize);\n }\n // Can we fit outside the workspace bounds (but inside the window)\n // below?\n if (primaryY + divSize.height < document.documentElement.clientHeight) {\n return getPositionBelowMetrics(primaryX, primaryY, boundsInfo, divSize);\n }\n // Can we fit outside the workspace bounds (but inside the window)\n // above?\n if (secondaryY - divSize.height > document.documentElement.clientTop) {\n return getPositionAboveMetrics(\n secondaryX, secondaryY, boundsInfo, divSize);\n }\n\n // Last resort, render at top of page.\n return getPositionTopOfPageMetrics(primaryX, boundsInfo, divSize);\n },\n};\n\n/**\n * Get the metrics for positioning the div below the source.\n *\n * @param primaryX Desired origin point x, in absolute px.\n * @param primaryY Desired origin point y, in absolute px.\n * @param boundsInfo An object containing size information about the bounding\n * element (bounding box and width/height).\n * @param divSize An object containing information about the size of the\n * DropDownDiv (width & height).\n * @returns Various final metrics, including rendered positions for drop-down\n * and arrow.\n */\nfunction getPositionBelowMetrics(\n primaryX: number, primaryY: number, boundsInfo: BoundsInfo,\n divSize: Size): PositionMetrics {\n const xCoords =\n getPositionX(primaryX, boundsInfo.left, boundsInfo.right, divSize.width);\n\n const arrowY = -(ARROW_SIZE / 2 + BORDER_SIZE);\n const finalY = primaryY + PADDING_Y;\n\n return {\n initialX: xCoords.divX,\n initialY: primaryY,\n finalX: xCoords.divX, // X position remains constant during animation.\n finalY,\n arrowX: xCoords.arrowX,\n arrowY,\n arrowAtTop: true,\n arrowVisible: true,\n };\n}\n\n/**\n * Get the metrics for positioning the div above the source.\n *\n * @param secondaryX Secondary/alternative origin point x, in absolute px.\n * @param secondaryY Secondary/alternative origin point y, in absolute px.\n * @param boundsInfo An object containing size information about the bounding\n * element (bounding box and width/height).\n * @param divSize An object containing information about the size of the\n * DropDownDiv (width & height).\n * @returns Various final metrics, including rendered positions for drop-down\n * and arrow.\n */\nfunction getPositionAboveMetrics(\n secondaryX: number, secondaryY: number, boundsInfo: BoundsInfo,\n divSize: Size): PositionMetrics {\n const xCoords = getPositionX(\n secondaryX, boundsInfo.left, boundsInfo.right, divSize.width);\n\n const arrowY = divSize.height - BORDER_SIZE * 2 - ARROW_SIZE / 2;\n const finalY = secondaryY - divSize.height - PADDING_Y;\n const initialY = secondaryY - divSize.height; // No padding on Y.\n\n return {\n initialX: xCoords.divX,\n initialY,\n finalX: xCoords.divX, // X position remains constant during animation.\n finalY,\n arrowX: xCoords.arrowX,\n arrowY,\n arrowAtTop: false,\n arrowVisible: true,\n };\n}\n\n/**\n * Get the metrics for positioning the div at the top of the page.\n *\n * @param sourceX Desired origin point x, in absolute px.\n * @param boundsInfo An object containing size information about the bounding\n * element (bounding box and width/height).\n * @param divSize An object containing information about the size of the\n * DropDownDiv (width & height).\n * @returns Various final metrics, including rendered positions for drop-down\n * and arrow.\n */\nfunction getPositionTopOfPageMetrics(\n sourceX: number, boundsInfo: BoundsInfo, divSize: Size): PositionMetrics {\n const xCoords =\n getPositionX(sourceX, boundsInfo.left, boundsInfo.right, divSize.width);\n\n // No need to provide arrow-specific information because it won't be visible.\n return {\n initialX: xCoords.divX,\n initialY: 0,\n finalX: xCoords.divX, // X position remains constant during animation.\n finalY: 0, // Y position remains constant during animation.\n arrowAtTop: null,\n arrowX: null,\n arrowY: null,\n arrowVisible: false,\n };\n}\n\n/**\n * Get the x positions for the left side of the DropDownDiv and the arrow,\n * accounting for the bounds of the workspace.\n *\n * @param sourceX Desired origin point x, in absolute px.\n * @param boundsLeft The left edge of the bounding element, in absolute px.\n * @param boundsRight The right edge of the bounding element, in absolute px.\n * @param divWidth The width of the div in px.\n * @returns An object containing metrics for the x positions of the left side of\n * the DropDownDiv and the arrow.\n * @internal\n */\nexport function getPositionX(\n sourceX: number, boundsLeft: number, boundsRight: number,\n divWidth: number): {divX: number, arrowX: number} {\n let divX = sourceX;\n // Offset the topLeft coord so that the dropdowndiv is centered.\n divX -= divWidth / 2;\n // Fit the dropdowndiv within the bounds of the workspace.\n divX = math.clamp(boundsLeft, divX, boundsRight - divWidth);\n\n let arrowX = sourceX;\n // Offset the arrow coord so that the arrow is centered.\n arrowX -= ARROW_SIZE / 2;\n // Convert the arrow position to be relative to the top left of the div.\n let relativeArrowX = arrowX - divX;\n const horizPadding = ARROW_HORIZONTAL_PADDING;\n // Clamp the arrow position so that it stays attached to the dropdowndiv.\n relativeArrowX = math.clamp(\n horizPadding, relativeArrowX, divWidth - horizPadding - ARROW_SIZE);\n\n return {arrowX: relativeArrowX, divX};\n}\n\n/**\n * Is the container visible?\n *\n * @returns True if visible.\n */\nexport function isVisible(): boolean {\n return !!owner;\n}\n\n/**\n * Hide the menu only if it is owned by the provided object.\n *\n * @param divOwner Object which must be owning the drop-down to hide.\n * @param opt_withoutAnimation True if we should hide the dropdown without\n * animating.\n * @returns True if hidden.\n */\nexport function hideIfOwner(\n divOwner: Field, opt_withoutAnimation?: boolean): boolean {\n if (owner === divOwner) {\n if (opt_withoutAnimation) {\n hideWithoutAnimation();\n } else {\n hide();\n }\n return true;\n }\n return false;\n}\n\n/** Hide the menu, triggering animation. */\nexport function hide() {\n // Start the animation by setting the translation and fading out.\n // Reset to (initialX, initialY) - i.e., no translation.\n div.style.transform = 'translate(0, 0)';\n div.style.opacity = '0';\n // Finish animation - reset all values to default.\n animateOutTimer = setTimeout(function() {\n hideWithoutAnimation();\n }, ANIMATION_TIME * 1000);\n if (onHide) {\n onHide();\n onHide = null;\n }\n}\n\n/** Hide the menu, without animation. */\nexport function hideWithoutAnimation() {\n if (!isVisible()) {\n return;\n }\n if (animateOutTimer) {\n clearTimeout(animateOutTimer);\n }\n\n // Reset style properties in case this gets called directly\n // instead of hide() - see discussion on #2551.\n div.style.transform = '';\n div.style.left = '';\n div.style.top = '';\n div.style.opacity = '0';\n div.style.display = 'none';\n div.style.backgroundColor = '';\n div.style.borderColor = '';\n\n if (onHide) {\n onHide();\n onHide = null;\n }\n clearContent();\n owner = null;\n\n if (renderedClassName) {\n dom.removeClass(div, renderedClassName);\n renderedClassName = '';\n }\n if (themeClassName) {\n dom.removeClass(div, themeClassName);\n themeClassName = '';\n }\n (common.getMainWorkspace() as WorkspaceSvg).markFocused();\n}\n\n/**\n * Set the dropdown div's position.\n *\n * @param primaryX Desired origin point x, in absolute px.\n * @param primaryY Desired origin point y, in absolute px.\n * @param secondaryX Secondary/alternative origin point x, in absolute px.\n * @param secondaryY Secondary/alternative origin point y, in absolute px.\n * @returns True if the menu rendered at the primary origin point.\n */\nfunction positionInternal(\n primaryX: number, primaryY: number, secondaryX: number,\n secondaryY: number): boolean {\n const metrics =\n internal.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY);\n\n // Update arrow CSS.\n if (metrics.arrowVisible) {\n arrow.style.display = '';\n arrow.style.transform = 'translate(' + metrics.arrowX + 'px,' +\n metrics.arrowY + 'px) rotate(45deg)';\n arrow.setAttribute(\n 'class',\n metrics.arrowAtTop ? 'blocklyDropDownArrow blocklyArrowTop' :\n 'blocklyDropDownArrow blocklyArrowBottom');\n } else {\n arrow.style.display = 'none';\n }\n\n const initialX = Math.floor(metrics.initialX);\n const initialY = Math.floor(metrics.initialY);\n const finalX = Math.floor(metrics.finalX);\n const finalY = Math.floor(metrics.finalY);\n\n // First apply initial translation.\n div.style.left = initialX + 'px';\n div.style.top = initialY + 'px';\n\n // Show the div.\n div.style.display = 'block';\n div.style.opacity = '1';\n // Add final translate, animated through `transition`.\n // Coordinates are relative to (initialX, initialY),\n // where the drop-down is absolutely positioned.\n const dx = finalX - initialX;\n const dy = finalY - initialY;\n div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';\n\n return !!metrics.arrowAtTop;\n}\n\n/**\n * Repositions the dropdownDiv on window resize. If it doesn't know how to\n * calculate the new position, it will just hide it instead.\n *\n * @internal\n */\nexport function repositionForWindowResize() {\n // This condition mainly catches the dropdown div when it is being used as a\n // dropdown. It is important not to close it in this case because on Android,\n // when a field is focused, the soft keyboard opens triggering a window resize\n // event and we want the dropdown div to stick around so users can type into\n // it.\n if (owner) {\n const block = owner.getSourceBlock() as BlockSvg;\n const bBox = positionToField ? getScaledBboxOfField(owner) :\n getScaledBboxOfBlock(block);\n // If we can fit it, render below the block.\n const primaryX = bBox.left + (bBox.right - bBox.left) / 2;\n const primaryY = bBox.bottom;\n // If we can't fit it, render above the entire parent block.\n const secondaryX = primaryX;\n const secondaryY = bBox.top;\n positionInternal(primaryX, primaryY, secondaryX, secondaryY);\n } else {\n hide();\n }\n}\n\nexport const TEST_ONLY = internal;\n","/**\n * @license\n * Copyright 2020 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * This file is a universal registry that provides generic methods\n * for registering and unregistering different types of classes.\n *\n * @namespace Blockly.registry\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.registry');\n\nimport type {Abstract} from './events/events_abstract.js';\nimport type {Field} from './field.js';\nimport type {IBlockDragger} from './interfaces/i_block_dragger.js';\nimport type {IConnectionChecker} from './interfaces/i_connection_checker.js';\nimport type {IFlyout} from './interfaces/i_flyout.js';\nimport type {IMetricsManager} from './interfaces/i_metrics_manager.js';\nimport type {ISerializer} from './interfaces/i_serializer.js';\nimport type {IToolbox} from './interfaces/i_toolbox.js';\nimport type {Cursor} from './keyboard_nav/cursor.js';\nimport type {Options} from './options.js';\nimport type {Renderer} from './renderers/common/renderer.js';\nimport type {Theme} from './theme.js';\nimport type {ToolboxItem} from './toolbox/toolbox_item.js';\n\n\n/**\n * A map of maps. With the keys being the type and name of the class we are\n * registering and the value being the constructor function.\n * e.g. {'field': {'field_angle': Blockly.FieldAngle}}\n */\nconst typeMap: {\n [key: string]:\n {[key: string]: (new () => AnyDuringMigration)|AnyDuringMigration}\n} = Object.create(null);\nexport const TEST_ONLY = {typeMap};\n\n/**\n * A map of maps. With the keys being the type and caseless name of the class we\n * are registring, and the value being the most recent cased name for that\n * registration.\n */\nconst nameMap: {[key: string]: {[key: string]: string}} = Object.create(null);\n\n/**\n * The string used to register the default class for a type of plugin.\n *\n * @alias Blockly.registry.DEFAULT\n */\nexport const DEFAULT = 'default';\n\n/**\n * A name with the type of the element stored in the generic.\n *\n * @alias Blockly.registry.Type\n */\nexport class Type<_T> {\n /** @param name The name of the registry type. */\n constructor(private readonly name: string) {}\n\n /**\n * Returns the name of the type.\n *\n * @returns The name.\n */\n toString(): string {\n return this.name;\n }\n\n static CONNECTION_CHECKER = new Type('connectionChecker');\n\n static CURSOR = new Type('cursor');\n\n static EVENT = new Type('event');\n\n static FIELD = new Type('field');\n\n static RENDERER = new Type('renderer');\n\n static TOOLBOX = new Type('toolbox');\n\n static THEME = new Type('theme');\n\n static TOOLBOX_ITEM = new Type('toolboxItem');\n\n static FLYOUTS_VERTICAL_TOOLBOX = new Type('flyoutsVerticalToolbox');\n\n static FLYOUTS_HORIZONTAL_TOOLBOX =\n new Type('flyoutsHorizontalToolbox');\n\n static METRICS_MANAGER = new Type('metricsManager');\n\n static BLOCK_DRAGGER = new Type('blockDragger');\n\n /** @internal */\n static SERIALIZER = new Type('serializer');\n}\n\n/**\n * Registers a class based on a type and name.\n *\n * @param type The type of the plugin.\n * (e.g. Field, Renderer)\n * @param name The plugin's name. (Ex. field_angle, geras)\n * @param registryItem The class or object to register.\n * @param opt_allowOverrides True to prevent an error when overriding an already\n * registered item.\n * @throws {Error} if the type or name is empty, a name with the given type has\n * already been registered, or if the given class or object is not valid for\n * its type.\n * @alias Blockly.registry.register\n */\nexport function register(\n type: string|Type, name: string,\n registryItem: (new (...p1: AnyDuringMigration[]) => T)|null|\n AnyDuringMigration,\n opt_allowOverrides?: boolean): void {\n if (!(type instanceof Type) && typeof type !== 'string' ||\n String(type).trim() === '') {\n throw Error(\n 'Invalid type \"' + type + '\". The type must be a' +\n ' non-empty string or a Blockly.registry.Type.');\n }\n type = String(type).toLowerCase();\n\n if (typeof name !== 'string' || name.trim() === '') {\n throw Error(\n 'Invalid name \"' + name + '\". The name must be a' +\n ' non-empty string.');\n }\n const caselessName = name.toLowerCase();\n if (!registryItem) {\n throw Error('Can not register a null value');\n }\n let typeRegistry = typeMap[type];\n let nameRegistry = nameMap[type];\n // If the type registry has not been created, create it.\n if (!typeRegistry) {\n typeRegistry = typeMap[type] = Object.create(null);\n nameRegistry = nameMap[type] = Object.create(null);\n }\n\n // Validate that the given class has all the required properties.\n validate(type, registryItem);\n\n // Don't throw an error if opt_allowOverrides is true.\n if (!opt_allowOverrides && typeRegistry[caselessName]) {\n throw Error(\n 'Name \"' + caselessName + '\" with type \"' + type +\n '\" already registered.');\n }\n typeRegistry[caselessName] = registryItem;\n nameRegistry[caselessName] = name;\n}\n\n/**\n * Checks the given registry item for properties that are required based on the\n * type.\n *\n * @param type The type of the plugin. (e.g. Field, Renderer)\n * @param registryItem A class or object that we are checking for the required\n * properties.\n */\nfunction validate(type: string, registryItem: Function|AnyDuringMigration) {\n switch (type) {\n case String(Type.FIELD):\n if (typeof registryItem.fromJson !== 'function') {\n throw Error('Type \"' + type + '\" must have a fromJson function');\n }\n break;\n }\n}\n\n/**\n * Unregisters the registry item with the given type and name.\n *\n * @param type The type of the plugin.\n * (e.g. Field, Renderer)\n * @param name The plugin's name. (Ex. field_angle, geras)\n * @alias Blockly.registry.unregister\n */\nexport function unregister(type: string|Type, name: string) {\n type = String(type).toLowerCase();\n name = name.toLowerCase();\n const typeRegistry = typeMap[type];\n if (!typeRegistry || !typeRegistry[name]) {\n console.warn(\n 'Unable to unregister [' + name + '][' + type + '] from the ' +\n 'registry.');\n return;\n }\n delete typeMap[type][name];\n delete nameMap[type][name];\n}\n\n/**\n * Gets the registry item for the given name and type. This can be either a\n * class or an object.\n *\n * @param type The type of the plugin.\n * (e.g. Field, Renderer)\n * @param name The plugin's name. (Ex. field_angle, geras)\n * @param opt_throwIfMissing Whether or not to throw an error if we are unable\n * to find the plugin.\n * @returns The class or object with the given name and type or null if none\n * exists.\n */\nfunction getItem(\n type: string|Type, name: string, opt_throwIfMissing?: boolean):\n (new (...p1: AnyDuringMigration[]) => T)|null|AnyDuringMigration {\n type = String(type).toLowerCase();\n name = name.toLowerCase();\n const typeRegistry = typeMap[type];\n if (!typeRegistry || !typeRegistry[name]) {\n const msg = 'Unable to find [' + name + '][' + type + '] in the registry.';\n if (opt_throwIfMissing) {\n throw new Error(\n msg + ' You must require or register a ' + type + ' plugin.');\n } else {\n console.warn(msg);\n }\n return null;\n }\n return typeRegistry[name];\n}\n\n/**\n * Returns whether or not the registry contains an item with the given type and\n * name.\n *\n * @param type The type of the plugin.\n * (e.g. Field, Renderer)\n * @param name The plugin's name. (Ex. field_angle, geras)\n * @returns True if the registry has an item with the given type and name, false\n * otherwise.\n * @alias Blockly.registry.hasItem\n */\nexport function hasItem(type: string|Type, name: string): boolean {\n type = String(type).toLowerCase();\n name = name.toLowerCase();\n const typeRegistry = typeMap[type];\n if (!typeRegistry) {\n return false;\n }\n return !!typeRegistry[name];\n}\n\n/**\n * Gets the class for the given name and type.\n *\n * @param type The type of the plugin.\n * (e.g. Field, Renderer)\n * @param name The plugin's name. (Ex. field_angle, geras)\n * @param opt_throwIfMissing Whether or not to throw an error if we are unable\n * to find the plugin.\n * @returns The class with the given name and type or null if none exists.\n * @alias Blockly.registry.getClass\n */\nexport function getClass(\n type: string|Type, name: string, opt_throwIfMissing?: boolean):\n (new (...p1: AnyDuringMigration[]) => T)|null {\n return getItem(type, name, opt_throwIfMissing) as (\n new (...p1: AnyDuringMigration[]) => T) |\n null;\n}\n\n/**\n * Gets the object for the given name and type.\n *\n * @param type The type of the plugin.\n * (e.g. Category)\n * @param name The plugin's name. (Ex. logic_category)\n * @param opt_throwIfMissing Whether or not to throw an error if we are unable\n * to find the object.\n * @returns The object with the given name and type or null if none exists.\n * @alias Blockly.registry.getObject\n */\nexport function getObject(\n type: string|Type, name: string, opt_throwIfMissing?: boolean): T|null {\n return getItem(type, name, opt_throwIfMissing) as T;\n}\n\n/**\n * Returns a map of items registered with the given type.\n *\n * @param type The type of the plugin. (e.g. Category)\n * @param opt_cased Whether or not to return a map with cased keys (rather than\n * caseless keys). False by default.\n * @param opt_throwIfMissing Whether or not to throw an error if we are unable\n * to find the object. False by default.\n * @returns A map of objects with the given type, or null if none exists.\n * @alias Blockly.registry.getAllItems\n */\nexport function getAllItems(\n type: string|Type, opt_cased?: boolean, opt_throwIfMissing?: boolean):\n {[key: string]: T|null|(new (...p1: AnyDuringMigration[]) => T)}|null {\n type = String(type).toLowerCase();\n const typeRegistry = typeMap[type];\n if (!typeRegistry) {\n const msg = `Unable to find [${type}] in the registry.`;\n if (opt_throwIfMissing) {\n throw new Error(`${msg} You must require or register a ${type} plugin.`);\n } else {\n console.warn(msg);\n }\n return null;\n }\n if (!opt_cased) {\n return typeRegistry;\n }\n const nameRegistry = nameMap[type];\n const casedRegistry = Object.create(null);\n const keys = Object.keys(typeRegistry);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n casedRegistry[nameRegistry[key]] = typeRegistry[key];\n }\n return casedRegistry;\n}\n\n/**\n * Gets the class from Blockly options for the given type.\n * This is used for plugins that override a built in feature. (e.g. Toolbox)\n *\n * @param type The type of the plugin.\n * @param options The option object to check for the given plugin.\n * @param opt_throwIfMissing Whether or not to throw an error if we are unable\n * to find the plugin.\n * @returns The class for the plugin.\n * @alias Blockly.registry.getClassFromOptions\n */\nexport function getClassFromOptions(\n type: Type, options: Options, opt_throwIfMissing?: boolean):\n (new (...p1: AnyDuringMigration[]) => T)|null {\n const typeName = type.toString();\n const plugin = options.plugins[typeName] || DEFAULT;\n\n // If the user passed in a plugin class instead of a registered plugin name.\n if (typeof plugin === 'function') {\n return plugin;\n }\n return getClass(type, plugin, opt_throwIfMissing);\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Generators for unique IDs.\n *\n * @namespace Blockly.utils.idGenerator\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.idGenerator');\n\n/**\n * Legal characters for the universally unique IDs. Should be all on\n * a US keyboard. No characters that conflict with XML or JSON.\n * Requests to remove additional 'problematic' characters from this\n * soup will be denied. That's your failure to properly escape in\n * your own environment. Issues #251, #625, #682, #1304.\n */\nconst soup = '!#$%()*+,-./:;=?@[]^_`{|}~' +\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n/**\n * Namespace object for internal implementations we want to be able to\n * stub in tests. Do not use externally.\n *\n * @internal\n */\nconst internal = {\n /**\n * Generate a random unique ID. This should be globally unique.\n * 87 characters ^ 20 length is greater than 128 bits (better than a UUID).\n *\n * @returns A globally unique ID string.\n */\n genUid: () => {\n const length = 20;\n const soupLength = soup.length;\n const id = [];\n for (let i = 0; i < length; i++) {\n id[i] = soup.charAt(Math.random() * soupLength);\n }\n return id.join('');\n },\n};\nexport const TEST_ONLY = internal;\n\n/** Next unique ID to use. */\nlet nextId = 0;\n\n/**\n * Generate the next unique element IDs.\n * IDs are compatible with the HTML4 'id' attribute restrictions:\n * Use only ASCII letters, digits, '_', '-' and '.'\n *\n * For UUIDs use genUid (below) instead; this ID generator should\n * primarily be used for IDs that end up in the DOM.\n *\n * @returns The next unique identifier.\n * @alias Blockly.utils.idGenerator.getNextUniqueId\n */\nexport function getNextUniqueId(): string {\n return 'blockly-' + (nextId++).toString(36);\n}\n\n/**\n * Generate a random unique ID.\n *\n * @see internal.genUid\n * @returns A globally unique ID string.\n * @alias Blockly.utils.idGenerator.genUid\n */\nexport function genUid(): string {\n return internal.genUid();\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Helper methods for events that are fired as a result of\n * actions in Blockly's editor.\n *\n * @namespace Blockly.Events.utils\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Events.utils');\n\nimport type {Block} from '../block.js';\nimport * as common from '../common.js';\nimport * as registry from '../registry.js';\nimport * as idGenerator from '../utils/idgenerator.js';\nimport type {Workspace} from '../workspace.js';\nimport type {WorkspaceSvg} from '../workspace_svg.js';\n\nimport type {Abstract} from './events_abstract.js';\nimport type {BlockChange} from './events_block_change.js';\nimport type {BlockCreate} from './events_block_create.js';\nimport type {BlockMove} from './events_block_move.js';\nimport type {CommentCreate} from './events_comment_create.js';\nimport type {CommentMove} from './events_comment_move.js';\nimport type {ViewportChange} from './events_viewport.js';\n\n\n/** Group ID for new events. Grouped events are indivisible. */\nlet group = '';\n\n/** Sets whether the next event should be added to the undo stack. */\nlet recordUndo = true;\n\n/**\n * Sets whether events should be added to the undo stack.\n *\n * @param newValue True if events should be added to the undo stack.\n * @alias Blockly.Events.utils.setRecordUndo\n */\nexport function setRecordUndo(newValue: boolean) {\n recordUndo = newValue;\n}\n\n/**\n * Returns whether or not events will be added to the undo stack.\n *\n * @returns True if events will be added to the undo stack.\n * @alias Blockly.Events.utils.getRecordUndo\n */\nexport function getRecordUndo(): boolean {\n return recordUndo;\n}\n\n/** Allow change events to be created and fired. */\nlet disabled = 0;\n\n/**\n * Name of event that creates a block. Will be deprecated for BLOCK_CREATE.\n *\n * @alias Blockly.Events.utils.CREATE\n */\nexport const CREATE = 'create';\n\n/**\n * Name of event that creates a block.\n *\n * @alias Blockly.Events.utils.BLOCK_CREATE\n */\nexport const BLOCK_CREATE = CREATE;\n\n/**\n * Name of event that deletes a block. Will be deprecated for BLOCK_DELETE.\n *\n * @alias Blockly.Events.utils.DELETE\n */\nexport const DELETE = 'delete';\n\n/**\n * Name of event that deletes a block.\n *\n * @alias Blockly.Events.utils.BLOCK_DELETE\n */\nexport const BLOCK_DELETE = DELETE;\n\n/**\n * Name of event that changes a block. Will be deprecated for BLOCK_CHANGE.\n *\n * @alias Blockly.Events.utils.CHANGE\n */\nexport const CHANGE = 'change';\n\n/**\n * Name of event that changes a block.\n *\n * @alias Blockly.Events.utils.BLOCK_CHANGE\n */\nexport const BLOCK_CHANGE = CHANGE;\n\n/**\n * Name of event that moves a block. Will be deprecated for BLOCK_MOVE.\n *\n * @alias Blockly.Events.utils.MOVE\n */\nexport const MOVE = 'move';\n\n/**\n * Name of event that moves a block.\n *\n * @alias Blockly.Events.utils.BLOCK_MOVE\n */\nexport const BLOCK_MOVE = MOVE;\n\n/**\n * Name of event that creates a variable.\n *\n * @alias Blockly.Events.utils.VAR_CREATE\n */\nexport const VAR_CREATE = 'var_create';\n\n/**\n * Name of event that deletes a variable.\n *\n * @alias Blockly.Events.utils.VAR_DELETE\n */\nexport const VAR_DELETE = 'var_delete';\n\n/**\n * Name of event that renames a variable.\n *\n * @alias Blockly.Events.utils.VAR_RENAME\n */\nexport const VAR_RENAME = 'var_rename';\n\n/**\n * Name of generic event that records a UI change.\n *\n * @alias Blockly.Events.utils.UI\n */\nexport const UI = 'ui';\n\n/**\n * Name of event that record a block drags a block.\n *\n * @alias Blockly.Events.utils.BLOCK_DRAG\n */\nexport const BLOCK_DRAG = 'drag';\n\n/**\n * Name of event that records a change in selected element.\n *\n * @alias Blockly.Events.utils.SELECTED\n */\nexport const SELECTED = 'selected';\n\n/**\n * Name of event that records a click.\n *\n * @alias Blockly.Events.utils.CLICK\n */\nexport const CLICK = 'click';\n\n/**\n * Name of event that records a marker move.\n *\n * @alias Blockly.Events.utils.MARKER_MOVE\n */\nexport const MARKER_MOVE = 'marker_move';\n\n/**\n * Name of event that records a bubble open.\n *\n * @alias Blockly.Events.utils.BUBBLE_OPEN\n */\nexport const BUBBLE_OPEN = 'bubble_open';\n\n/**\n * Name of event that records a trashcan open.\n *\n * @alias Blockly.Events.utils.TRASHCAN_OPEN\n */\nexport const TRASHCAN_OPEN = 'trashcan_open';\n\n/**\n * Name of event that records a toolbox item select.\n *\n * @alias Blockly.Events.utils.TOOLBOX_ITEM_SELECT\n */\nexport const TOOLBOX_ITEM_SELECT = 'toolbox_item_select';\n\n/**\n * Name of event that records a theme change.\n *\n * @alias Blockly.Events.utils.THEME_CHANGE\n */\nexport const THEME_CHANGE = 'theme_change';\n\n/**\n * Name of event that records a viewport change.\n *\n * @alias Blockly.Events.utils.VIEWPORT_CHANGE\n */\nexport const VIEWPORT_CHANGE = 'viewport_change';\n\n/**\n * Name of event that creates a comment.\n *\n * @alias Blockly.Events.utils.COMMENT_CREATE\n */\nexport const COMMENT_CREATE = 'comment_create';\n\n/**\n * Name of event that deletes a comment.\n *\n * @alias Blockly.Events.utils.COMMENT_DELETE\n */\nexport const COMMENT_DELETE = 'comment_delete';\n\n/**\n * Name of event that changes a comment.\n *\n * @alias Blockly.Events.utils.COMMENT_CHANGE\n */\nexport const COMMENT_CHANGE = 'comment_change';\n\n/**\n * Name of event that moves a comment.\n *\n * @alias Blockly.Events.utils.COMMENT_MOVE\n */\nexport const COMMENT_MOVE = 'comment_move';\n\n/**\n * Name of event that records a workspace load.\n *\n * @alias Blockly.Events.utils.FINISHED_LOADING\n */\nexport const FINISHED_LOADING = 'finished_loading';\n\n/**\n * Type of events that cause objects to be bumped back into the visible\n * portion of the workspace.\n *\n * Not to be confused with bumping so that disconnected connections do not\n * appear connected.\n *\n * @alias Blockly.Events.utils.BumpEvent\n */\nexport type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;\n\n/**\n * List of events that cause objects to be bumped back into the visible\n * portion of the workspace.\n *\n * Not to be confused with bumping so that disconnected connections do not\n * appear connected.\n *\n * @alias Blockly.Events.utils.BUMP_EVENTS\n */\nexport const BUMP_EVENTS: string[] =\n [BLOCK_CREATE, BLOCK_MOVE, COMMENT_CREATE, COMMENT_MOVE];\n\n/** List of events queued for firing. */\nconst FIRE_QUEUE: Abstract[] = [];\n\n/**\n * Create a custom event and fire it.\n *\n * @param event Custom data for event.\n * @alias Blockly.Events.utils.fire\n */\nexport function fire(event: Abstract) {\n TEST_ONLY.fireInternal(event);\n}\n\n/**\n * Private version of fireInternal for stubbing in tests.\n */\nfunction fireInternal(event: Abstract) {\n if (!isEnabled()) {\n return;\n }\n if (!FIRE_QUEUE.length) {\n // First event added; schedule a firing of the event queue.\n setTimeout(fireNow, 0);\n }\n FIRE_QUEUE.push(event);\n}\n\n\n/** Fire all queued events. */\nfunction fireNow() {\n const queue = filter(FIRE_QUEUE, true);\n FIRE_QUEUE.length = 0;\n for (let i = 0, event; event = queue[i]; i++) {\n if (!event.workspaceId) {\n continue;\n }\n const eventWorkspace = common.getWorkspaceById(event.workspaceId);\n if (eventWorkspace) {\n eventWorkspace.fireChangeListener(event);\n }\n }\n}\n\n/**\n * Filter the queued events and merge duplicates.\n *\n * @param queueIn Array of events.\n * @param forward True if forward (redo), false if backward (undo).\n * @returns Array of filtered events.\n * @alias Blockly.Events.utils.filter\n */\nexport function filter(queueIn: Abstract[], forward: boolean): Abstract[] {\n let queue = queueIn.slice();\n // Shallow copy of queue.\n if (!forward) {\n // Undo is merged in reverse order.\n queue.reverse();\n }\n const mergedQueue = [];\n const hash = Object.create(null);\n // Merge duplicates.\n for (let i = 0, event; event = queue[i]; i++) {\n if (!event.isNull()) {\n // Treat all UI events as the same type in hash table.\n const eventType = event.isUiEvent ? UI : event.type;\n // TODO(#5927): Check whether `blockId` exists before accessing it.\n const blockId = (event as AnyDuringMigration).blockId;\n const key = [eventType, blockId, event.workspaceId].join(' ');\n\n const lastEntry = hash[key];\n const lastEvent = lastEntry ? lastEntry.event : null;\n if (!lastEntry) {\n // Each item in the hash table has the event and the index of that event\n // in the input array. This lets us make sure we only merge adjacent\n // move events.\n hash[key] = {event, index: i};\n mergedQueue.push(event);\n } else if (event.type === MOVE && lastEntry.index === i - 1) {\n const moveEvent = event as BlockMove;\n // Merge move events.\n lastEvent.newParentId = moveEvent.newParentId;\n lastEvent.newInputName = moveEvent.newInputName;\n lastEvent.newCoordinate = moveEvent.newCoordinate;\n lastEntry.index = i;\n } else if (\n event.type === CHANGE &&\n (event as BlockChange).element === lastEvent.element &&\n (event as BlockChange).name === lastEvent.name) {\n const changeEvent = event as BlockChange;\n // Merge change events.\n lastEvent.newValue = changeEvent.newValue;\n } else if (event.type === VIEWPORT_CHANGE) {\n const viewportEvent = event as ViewportChange;\n // Merge viewport change events.\n lastEvent.viewTop = viewportEvent.viewTop;\n lastEvent.viewLeft = viewportEvent.viewLeft;\n lastEvent.scale = viewportEvent.scale;\n lastEvent.oldScale = viewportEvent.oldScale;\n } else if (event.type === CLICK && lastEvent.type === BUBBLE_OPEN) {\n // Drop click events caused by opening/closing bubbles.\n } else {\n // Collision: newer events should merge into this event to maintain\n // order.\n hash[key] = {event, index: i};\n mergedQueue.push(event);\n }\n }\n }\n // Filter out any events that have become null due to merging.\n queue = mergedQueue.filter(function(e) {\n return !e.isNull();\n });\n if (!forward) {\n // Restore undo order.\n queue.reverse();\n }\n // Move mutation events to the top of the queue.\n // Intentionally skip first event.\n for (let i = 1, event; event = queue[i]; i++) {\n // AnyDuringMigration because: Property 'element' does not exist on type\n // 'Abstract'.\n if (event.type === CHANGE &&\n (event as AnyDuringMigration).element === 'mutation') {\n queue.unshift(queue.splice(i, 1)[0]);\n }\n }\n return queue;\n}\n\n/**\n * Modify pending undo events so that when they are fired they don't land\n * in the undo stack. Called by Workspace.clearUndo.\n *\n * @alias Blockly.Events.utils.clearPendingUndo\n */\nexport function clearPendingUndo() {\n for (let i = 0, event; event = FIRE_QUEUE[i]; i++) {\n event.recordUndo = false;\n }\n}\n\n/**\n * Stop sending events. Every call to this function MUST also call enable.\n *\n * @alias Blockly.Events.utils.disable\n */\nexport function disable() {\n disabled++;\n}\n\n/**\n * Start sending events. Unless events were already disabled when the\n * corresponding call to disable was made.\n *\n * @alias Blockly.Events.utils.enable\n */\nexport function enable() {\n disabled--;\n}\n\n/**\n * Returns whether events may be fired or not.\n *\n * @returns True if enabled.\n * @alias Blockly.Events.utils.isEnabled\n */\nexport function isEnabled(): boolean {\n return disabled === 0;\n}\n\n/**\n * Current group.\n *\n * @returns ID string.\n * @alias Blockly.Events.utils.getGroup\n */\nexport function getGroup(): string {\n return group;\n}\n\n/**\n * Start or stop a group.\n *\n * @param state True to start new group, false to end group.\n * String to set group explicitly.\n * @alias Blockly.Events.utils.setGroup\n */\nexport function setGroup(state: boolean|string) {\n TEST_ONLY.setGroupInternal(state);\n}\n\n/**\n * Private version of setGroup for stubbing in tests.\n */\nfunction setGroupInternal(state: boolean|string) {\n if (typeof state === 'boolean') {\n group = state ? idGenerator.genUid() : '';\n } else {\n group = state;\n }\n}\n\n/**\n * Compute a list of the IDs of the specified block and all its descendants.\n *\n * @param block The root block.\n * @returns List of block IDs.\n * @alias Blockly.Events.utils.getDescendantIds\n * @internal\n */\nexport function getDescendantIds(block: Block): string[] {\n const ids = [];\n const descendants = block.getDescendants(false);\n for (let i = 0, descendant; descendant = descendants[i]; i++) {\n ids[i] = descendant.id;\n }\n return ids;\n}\n\n/**\n * Decode the JSON into an event.\n *\n * @param json JSON representation.\n * @param workspace Target workspace for event.\n * @returns The event represented by the JSON.\n * @throws {Error} if an event type is not found in the registry.\n * @alias Blockly.Events.utils.fromJson\n */\nexport function fromJson(\n json: AnyDuringMigration, workspace: Workspace): Abstract {\n const eventClass = get(json['type']);\n if (!eventClass) {\n throw Error('Unknown event type.');\n }\n const event = new eventClass();\n event.fromJson(json);\n event.workspaceId = workspace.id;\n return event;\n}\n\n/**\n * Gets the class for a specific event type from the registry.\n *\n * @param eventType The type of the event to get.\n * @returns The event class with the given type.\n * @alias Blockly.Events.utils.get\n */\nexport function get(eventType: string):\n (new (...p1: AnyDuringMigration[]) => Abstract) {\n const event = registry.getClass(registry.Type.EVENT, eventType);\n if (!event) {\n throw new Error(`Event type ${eventType} not found in registry.`);\n }\n return event;\n}\n\n/**\n * Enable/disable a block depending on whether it is properly connected.\n * Use this on applications where all blocks should be connected to a top block.\n * Recommend setting the 'disable' option to 'false' in the config so that\n * users don't try to re-enable disabled orphan blocks.\n *\n * @param event Custom data for event.\n * @alias Blockly.Events.utils.disableOrphans\n */\nexport function disableOrphans(event: Abstract) {\n if (event.type === MOVE || event.type === CREATE) {\n const blockEvent = event as BlockMove | BlockCreate;\n if (!blockEvent.workspaceId) {\n return;\n }\n const eventWorkspace =\n common.getWorkspaceById(blockEvent.workspaceId) as WorkspaceSvg;\n if (!blockEvent.blockId) {\n throw new Error('Encountered a blockEvent without a proper blockId');\n }\n let block = eventWorkspace.getBlockById(blockEvent.blockId);\n if (block) {\n // Changing blocks as part of this event shouldn't be undoable.\n const initialUndoFlag = recordUndo;\n try {\n recordUndo = false;\n const parent = block.getParent();\n if (parent && parent.isEnabled()) {\n const children = block.getDescendants(false);\n for (let i = 0, child; child = children[i]; i++) {\n child.setEnabled(true);\n }\n } else if (\n (block.outputConnection || block.previousConnection) &&\n !eventWorkspace.isDragging()) {\n do {\n block.setEnabled(false);\n block = block.getNextBlock();\n } while (block);\n }\n } finally {\n recordUndo = initialUndoFlag;\n }\n }\n }\n}\n\nexport const TEST_ONLY = {\n FIRE_QUEUE,\n fireNow,\n fireInternal,\n setGroupInternal,\n};\n","/**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * XML element manipulation.\n * These methods are not specific to Blockly, and could be factored out into\n * a JavaScript framework such as Closure.\n *\n * @namespace Blockly.utils.xml\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.xml');\n\n\n/**\n * Namespace for Blockly's XML.\n *\n * @alias Blockly.utils.xml.NAME_SPACE\n */\nexport const NAME_SPACE = 'https://developers.google.com/blockly/xml';\n\n/**\n * The Document object to use. By default this is just document, but\n * the Node.js build of Blockly (see scripts/package/node/core.js)\n * calls setDocument to supply a Document implementation from the\n * jsdom package instead.\n */\nlet xmlDocument: Document = globalThis['document'];\n\n/**\n * Get the document object to use for XML serialization.\n *\n * @returns The document object.\n * @alias Blockly.utils.xml.getDocument\n */\nexport function getDocument(): Document {\n return xmlDocument;\n}\n\n/**\n * Get the document object to use for XML serialization.\n *\n * @param document The document object to use.\n * @alias Blockly.utils.xml.setDocument\n */\nexport function setDocument(document: Document) {\n xmlDocument = document;\n}\n\n/**\n * Create DOM element for XML.\n *\n * @param tagName Name of DOM element.\n * @returns New DOM element.\n * @alias Blockly.utils.xml.createElement\n */\nexport function createElement(tagName: string): Element {\n return xmlDocument.createElementNS(NAME_SPACE, tagName);\n}\n\n/**\n * Create text element for XML.\n *\n * @param text Text content.\n * @returns New DOM text node.\n * @alias Blockly.utils.xml.createTextNode\n */\nexport function createTextNode(text: string): Text {\n return xmlDocument.createTextNode(text);\n}\n\n/**\n * Converts an XML string into a DOM tree.\n *\n * @param text XML string.\n * @returns The DOM document.\n * @throws if XML doesn't parse.\n * @alias Blockly.utils.xml.textToDomDocument\n */\nexport function textToDomDocument(text: string): Document {\n const oParser = new DOMParser();\n return oParser.parseFromString(text, 'text/xml');\n}\n\n/**\n * Converts a DOM structure into plain text.\n * Currently the text format is fairly ugly: all one line with no whitespace.\n *\n * @param dom A tree of XML nodes.\n * @returns Text representation.\n * @alias Blockly.utils.xml.domToText\n */\nexport function domToText(dom: Node): string {\n const oSerializer = new XMLSerializer();\n return oSerializer.serializeToString(dom);\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Wrapper functions around JS functions for showing alert/confirmation dialogs.\n *\n * @namespace Blockly.dialog\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.dialog');\n\n\nlet alertImplementation = function(message: string, opt_callback?: () => void) {\n window.alert(message);\n if (opt_callback) {\n opt_callback();\n }\n};\n\nlet confirmImplementation = function(\n message: string, callback: (result: boolean) => void) {\n callback(window.confirm(message));\n};\n\nlet promptImplementation = function(\n message: string, defaultValue: string,\n callback: (result: string|null) => void) {\n callback(window.prompt(message, defaultValue));\n};\n\n/**\n * Wrapper to window.alert() that app developers may override via setAlert to\n * provide alternatives to the modal browser window.\n *\n * @param message The message to display to the user.\n * @param opt_callback The callback when the alert is dismissed.\n * @alias Blockly.dialog.alert\n */\nexport function alert(message: string, opt_callback?: () => void) {\n alertImplementation(message, opt_callback);\n}\n\n/**\n * Sets the function to be run when Blockly.dialog.alert() is called.\n *\n * @param alertFunction The function to be run.\n * @see Blockly.dialog.alert\n * @alias Blockly.dialog.setAlert\n */\nexport function setAlert(alertFunction: (p1: string, p2?: () => void) => void) {\n alertImplementation = alertFunction;\n}\n\n/**\n * Wrapper to window.confirm() that app developers may override via setConfirm\n * to provide alternatives to the modal browser window.\n *\n * @param message The message to display to the user.\n * @param callback The callback for handling user response.\n * @alias Blockly.dialog.confirm\n */\nexport function confirm(message: string, callback: (p1: boolean) => void) {\n TEST_ONLY.confirmInternal(message, callback);\n}\n\n/**\n * Private version of confirm for stubbing in tests.\n */\nfunction confirmInternal(message: string, callback: (p1: boolean) => void) {\n confirmImplementation(message, callback);\n}\n\n\n/**\n * Sets the function to be run when Blockly.dialog.confirm() is called.\n *\n * @param confirmFunction The function to be run.\n * @see Blockly.dialog.confirm\n * @alias Blockly.dialog.setConfirm\n */\nexport function setConfirm(\n confirmFunction: (p1: string, p2: (p1: boolean) => void) => void) {\n confirmImplementation = confirmFunction;\n}\n\n/**\n * Wrapper to window.prompt() that app developers may override via setPrompt to\n * provide alternatives to the modal browser window. Built-in browser prompts\n * are often used for better text input experience on mobile device. We strongly\n * recommend testing mobile when overriding this.\n *\n * @param message The message to display to the user.\n * @param defaultValue The value to initialize the prompt with.\n * @param callback The callback for handling user response.\n * @alias Blockly.dialog.prompt\n */\nexport function prompt(\n message: string, defaultValue: string,\n callback: (p1: string|null) => void) {\n promptImplementation(message, defaultValue, callback);\n}\n\n/**\n * Sets the function to be run when Blockly.dialog.prompt() is called.\n *\n * @param promptFunction The function to be run.\n * @see Blockly.dialog.prompt\n * @alias Blockly.dialog.setPrompt\n */\nexport function setPrompt(\n promptFunction: (p1: string, p2: string, p3: (p1: string|null) => void) =>\n void) {\n promptImplementation = promptFunction;\n}\n\nexport const TEST_ONLY = {\n confirmInternal,\n};\n","/**\n * @license\n * Copyright 2012 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility functions for handling variables.\n *\n * @namespace Blockly.Variables\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Variables');\n\nimport {Blocks} from './blocks.js';\nimport * as dialog from './dialog.js';\nimport {Msg} from './msg.js';\nimport * as utilsXml from './utils/xml.js';\nimport {VariableModel} from './variable_model.js';\nimport type {Workspace} from './workspace.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\nimport * as Xml from './xml.js';\n\n\n/**\n * String for use in the \"custom\" attribute of a category in toolbox XML.\n * This string indicates that the category should be dynamically populated with\n * variable blocks.\n * See also Blockly.Procedures.CATEGORY_NAME and\n * Blockly.VariablesDynamic.CATEGORY_NAME.\n *\n * @alias Blockly.Variables.CATEGORY_NAME\n */\nexport const CATEGORY_NAME = 'VARIABLE';\n\n/**\n * Find all user-created variables that are in use in the workspace.\n * For use by generators.\n * To get a list of all variables on a workspace, including unused variables,\n * call Workspace.getAllVariables.\n *\n * @param ws The workspace to search for variables.\n * @returns Array of variable models.\n * @alias Blockly.Variables.allUsedVarModels\n */\nexport function allUsedVarModels(ws: Workspace): VariableModel[] {\n const blocks = ws.getAllBlocks(false);\n const variables = new Set();\n // Iterate through every block and add each variable to the set.\n for (let i = 0; i < blocks.length; i++) {\n const blockVariables = blocks[i].getVarModels();\n if (blockVariables) {\n for (let j = 0; j < blockVariables.length; j++) {\n const variable = blockVariables[j];\n const id = variable.getId();\n if (id) {\n variables.add(variable);\n }\n }\n }\n }\n // Convert the set into a list.\n return Array.from(variables.values());\n}\n\n/**\n * Find all developer variables used by blocks in the workspace.\n * Developer variables are never shown to the user, but are declared as global\n * variables in the generated code.\n * To declare developer variables, define the getDeveloperVariables function on\n * your block and return a list of variable names.\n * For use by generators.\n *\n * @param workspace The workspace to search.\n * @returns A list of non-duplicated variable names.\n * @alias Blockly.Variables.allDeveloperVariables\n */\nexport function allDeveloperVariables(workspace: Workspace): string[] {\n const blocks = workspace.getAllBlocks(false);\n const variables = new Set();\n for (let i = 0, block; block = blocks[i]; i++) {\n const getDeveloperVariables = block.getDeveloperVariables;\n if (getDeveloperVariables) {\n const devVars = getDeveloperVariables();\n for (let j = 0; j < devVars.length; j++) {\n variables.add(devVars[j]);\n }\n }\n }\n // Convert the set into a list.\n return Array.from(variables.values());\n}\n\n/**\n * Construct the elements (blocks and button) required by the flyout for the\n * variable category.\n *\n * @param workspace The workspace containing variables.\n * @returns Array of XML elements.\n * @alias Blockly.Variables.flyoutCategory\n */\nexport function flyoutCategory(workspace: WorkspaceSvg): Element[] {\n let xmlList = new Array();\n const button = document.createElement('button');\n button.setAttribute('text', '%{BKY_NEW_VARIABLE}');\n button.setAttribute('callbackKey', 'CREATE_VARIABLE');\n\n workspace.registerButtonCallback('CREATE_VARIABLE', function(button) {\n createVariableButtonHandler(button.getTargetWorkspace());\n });\n\n xmlList.push(button);\n\n const blockList = flyoutCategoryBlocks(workspace);\n xmlList = xmlList.concat(blockList);\n return xmlList;\n}\n\n/**\n * Construct the blocks required by the flyout for the variable category.\n *\n * @param workspace The workspace containing variables.\n * @returns Array of XML block elements.\n * @alias Blockly.Variables.flyoutCategoryBlocks\n */\nexport function flyoutCategoryBlocks(workspace: Workspace): Element[] {\n const variableModelList = workspace.getVariablesOfType('');\n\n const xmlList = [];\n if (variableModelList.length > 0) {\n // New variables are added to the end of the variableModelList.\n const mostRecentVariable = variableModelList[variableModelList.length - 1];\n if (Blocks['variables_set']) {\n const block = utilsXml.createElement('block');\n block.setAttribute('type', 'variables_set');\n block.setAttribute('gap', Blocks['math_change'] ? '8' : '24');\n block.appendChild(generateVariableFieldDom(mostRecentVariable));\n xmlList.push(block);\n }\n if (Blocks['math_change']) {\n const block = utilsXml.createElement('block');\n block.setAttribute('type', 'math_change');\n block.setAttribute('gap', Blocks['variables_get'] ? '20' : '8');\n block.appendChild(generateVariableFieldDom(mostRecentVariable));\n const value = Xml.textToDom(\n '' +\n '' +\n '1' +\n '' +\n '');\n block.appendChild(value);\n xmlList.push(block);\n }\n\n if (Blocks['variables_get']) {\n variableModelList.sort(VariableModel.compareByName);\n for (let i = 0, variable; variable = variableModelList[i]; i++) {\n const block = utilsXml.createElement('block');\n block.setAttribute('type', 'variables_get');\n block.setAttribute('gap', '8');\n block.appendChild(generateVariableFieldDom(variable));\n xmlList.push(block);\n }\n }\n }\n return xmlList;\n}\n\n/** @alias Blockly.Variables.VAR_LETTER_OPTIONS */\nexport const VAR_LETTER_OPTIONS = 'ijkmnopqrstuvwxyzabcdefgh';\n\n/**\n * Return a new variable name that is not yet being used. This will try to\n * generate single letter variable names in the range 'i' to 'z' to start with.\n * If no unique name is located it will try 'i' to 'z', 'a' to 'h',\n * then 'i2' to 'z2' etc. Skip 'l'.\n *\n * @param workspace The workspace to be unique in.\n * @returns New variable name.\n * @alias Blockly.Variables.generateUniqueName\n */\nexport function generateUniqueName(workspace: Workspace): string {\n return TEST_ONLY.generateUniqueNameInternal(workspace);\n}\n\n/**\n * Private version of generateUniqueName for stubbing in tests.\n */\nfunction generateUniqueNameInternal(workspace: Workspace): string {\n return generateUniqueNameFromOptions(\n VAR_LETTER_OPTIONS.charAt(0), workspace.getAllVariableNames());\n}\n\n/**\n * Returns a unique name that is not present in the usedNames array. This\n * will try to generate single letter names in the range a - z (skip l). It\n * will start with the character passed to startChar.\n *\n * @param startChar The character to start the search at.\n * @param usedNames A list of all of the used names.\n * @returns A unique name that is not present in the usedNames array.\n * @alias Blockly.Variables.generateUniqueNameFromOptions\n */\nexport function generateUniqueNameFromOptions(\n startChar: string, usedNames: string[]): string {\n if (!usedNames.length) {\n return startChar;\n }\n\n const letters = VAR_LETTER_OPTIONS;\n let suffix = '';\n let letterIndex = letters.indexOf(startChar);\n let potName = startChar;\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n let inUse = false;\n for (let i = 0; i < usedNames.length; i++) {\n if (usedNames[i].toLowerCase() === potName) {\n inUse = true;\n break;\n }\n }\n if (!inUse) {\n return potName;\n }\n\n letterIndex++;\n if (letterIndex === letters.length) {\n // Reached the end of the character sequence so back to 'i'.\n letterIndex = 0;\n suffix = `${Number(suffix) + 1}`;\n }\n potName = letters.charAt(letterIndex) + suffix;\n }\n}\n\n/**\n * Handles \"Create Variable\" button in the default variables toolbox category.\n * It will prompt the user for a variable name, including re-prompts if a name\n * is already in use among the workspace's variables.\n *\n * Custom button handlers can delegate to this function, allowing variables\n * types and after-creation processing. More complex customization (e.g.,\n * prompting for variable type) is beyond the scope of this function.\n *\n * @param workspace The workspace on which to create the variable.\n * @param opt_callback A callback. It will be passed an acceptable new variable\n * name, or null if change is to be aborted (cancel button), or undefined if\n * an existing variable was chosen.\n * @param opt_type The type of the variable like 'int', 'string', or ''. This\n * will default to '', which is a specific type.\n * @alias Blockly.Variables.createVariableButtonHandler\n */\nexport function createVariableButtonHandler(\n workspace: Workspace, opt_callback?: (p1?: string|null) => void,\n opt_type?: string) {\n const type = opt_type || '';\n // This function needs to be named so it can be called recursively.\n function promptAndCheckWithAlert(defaultName: string) {\n promptName(Msg['NEW_VARIABLE_TITLE'], defaultName, function(text) {\n if (text) {\n const existing = nameUsedWithAnyType(text, workspace);\n if (existing) {\n let msg;\n if (existing.type === type) {\n msg = Msg['VARIABLE_ALREADY_EXISTS'].replace('%1', existing.name);\n } else {\n msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE'];\n msg = msg.replace('%1', existing.name).replace('%2', existing.type);\n }\n dialog.alert(msg, function() {\n promptAndCheckWithAlert(text);\n });\n } else {\n // No conflict\n workspace.createVariable(text, type);\n if (opt_callback) {\n opt_callback(text);\n }\n }\n } else {\n // User canceled prompt.\n if (opt_callback) {\n opt_callback(null);\n }\n }\n });\n }\n promptAndCheckWithAlert('');\n}\n\n/**\n * Opens a prompt that allows the user to enter a new name for a variable.\n * Triggers a rename if the new name is valid. Or re-prompts if there is a\n * collision.\n *\n * @param workspace The workspace on which to rename the variable.\n * @param variable Variable to rename.\n * @param opt_callback A callback. It will be passed an acceptable new variable\n * name, or null if change is to be aborted (cancel button), or undefined if\n * an existing variable was chosen.\n * @alias Blockly.Variables.renameVariable\n */\nexport function renameVariable(\n workspace: Workspace, variable: VariableModel,\n opt_callback?: (p1?: string|null) => void) {\n // This function needs to be named so it can be called recursively.\n function promptAndCheckWithAlert(defaultName: string) {\n const promptText =\n Msg['RENAME_VARIABLE_TITLE'].replace('%1', variable.name);\n promptName(promptText, defaultName, function(newName) {\n if (newName) {\n const existing =\n nameUsedWithOtherType(newName, variable.type, workspace);\n if (existing) {\n const msg = Msg['VARIABLE_ALREADY_EXISTS_FOR_ANOTHER_TYPE']\n .replace('%1', existing.name)\n .replace('%2', existing.type);\n dialog.alert(msg, function() {\n promptAndCheckWithAlert(newName);\n });\n } else {\n workspace.renameVariableById(variable.getId(), newName);\n if (opt_callback) {\n opt_callback(newName);\n }\n }\n } else {\n // User canceled prompt.\n if (opt_callback) {\n opt_callback(null);\n }\n }\n });\n }\n promptAndCheckWithAlert('');\n}\n\n/**\n * Prompt the user for a new variable name.\n *\n * @param promptText The string of the prompt.\n * @param defaultText The default value to show in the prompt's field.\n * @param callback A callback. It will be passed the new variable name, or null\n * if the user picked something illegal.\n * @alias Blockly.Variables.promptName\n */\nexport function promptName(\n promptText: string, defaultText: string,\n callback: (p1: string|null) => void) {\n dialog.prompt(promptText, defaultText, function(newVar) {\n // Merge runs of whitespace. Strip leading and trailing whitespace.\n // Beyond this, all names are legal.\n if (newVar) {\n newVar = newVar.replace(/[\\s\\xa0]+/g, ' ').trim();\n if (newVar === Msg['RENAME_VARIABLE'] || newVar === Msg['NEW_VARIABLE']) {\n // Ok, not ALL names are legal...\n newVar = null;\n }\n }\n callback(newVar);\n });\n}\n/**\n * Check whether there exists a variable with the given name but a different\n * type.\n *\n * @param name The name to search for.\n * @param type The type to exclude from the search.\n * @param workspace The workspace to search for the variable.\n * @returns The variable with the given name and a different type, or null if\n * none was found.\n */\nfunction nameUsedWithOtherType(\n name: string, type: string, workspace: Workspace): VariableModel|null {\n const allVariables = workspace.getVariableMap().getAllVariables();\n\n name = name.toLowerCase();\n for (let i = 0, variable; variable = allVariables[i]; i++) {\n if (variable.name.toLowerCase() === name && variable.type !== type) {\n return variable;\n }\n }\n return null;\n}\n\n/**\n * Check whether there exists a variable with the given name of any type.\n *\n * @param name The name to search for.\n * @param workspace The workspace to search for the variable.\n * @returns The variable with the given name, or null if none was found.\n * @alias Blockly.Variables.nameUsedWithAnyType\n */\nexport function nameUsedWithAnyType(\n name: string, workspace: Workspace): VariableModel|null {\n const allVariables = workspace.getVariableMap().getAllVariables();\n\n name = name.toLowerCase();\n for (let i = 0, variable; variable = allVariables[i]; i++) {\n if (variable.name.toLowerCase() === name) {\n return variable;\n }\n }\n return null;\n}\n\n/**\n * Generate DOM objects representing a variable field.\n *\n * @param variableModel The variable model to represent.\n * @returns The generated DOM.\n * @alias Blockly.Variables.generateVariableFieldDom\n */\nexport function generateVariableFieldDom(variableModel: VariableModel):\n Element {\n /* Generates the following XML:\n * foo\n */\n const field = utilsXml.createElement('field');\n field.setAttribute('name', 'VAR');\n field.setAttribute('id', variableModel.getId());\n field.setAttribute('variabletype', variableModel.type);\n const name = utilsXml.createTextNode(variableModel.name);\n field.appendChild(name);\n return field;\n}\n\n/**\n * Helper function to look up or create a variable on the given workspace.\n * If no variable exists, creates and returns it.\n *\n * @param workspace The workspace to search for the variable. It may be a\n * flyout workspace or main workspace.\n * @param id The ID to use to look up or create the variable, or null.\n * @param opt_name The string to use to look up or create the variable.\n * @param opt_type The type to use to look up or create the variable.\n * @returns The variable corresponding to the given ID or name + type\n * combination.\n * @alias Blockly.Variables.getOrCreateVariablePackage\n */\nexport function getOrCreateVariablePackage(\n workspace: Workspace, id: string|null, opt_name?: string,\n opt_type?: string): VariableModel {\n let variable = getVariable(workspace, id, opt_name, opt_type);\n if (!variable) {\n variable = createVariable(workspace, id, opt_name, opt_type);\n }\n return variable;\n}\n\n/**\n * Look up a variable on the given workspace.\n * Always looks in the main workspace before looking in the flyout workspace.\n * Always prefers lookup by ID to lookup by name + type.\n *\n * @param workspace The workspace to search for the variable. It may be a\n * flyout workspace or main workspace.\n * @param id The ID to use to look up the variable, or null.\n * @param opt_name The string to use to look up the variable.\n * Only used if lookup by ID fails.\n * @param opt_type The type to use to look up the variable.\n * Only used if lookup by ID fails.\n * @returns The variable corresponding to the given ID or name + type\n * combination, or null if not found.\n * @alias Blockly.Variables.getVariable\n */\nexport function getVariable(\n workspace: Workspace, id: string|null, opt_name?: string,\n opt_type?: string): VariableModel|null {\n const potentialVariableMap = workspace.getPotentialVariableMap();\n let variable = null;\n // Try to just get the variable, by ID if possible.\n if (id) {\n // Look in the real variable map before checking the potential variable map.\n variable = workspace.getVariableById(id);\n if (!variable && potentialVariableMap) {\n variable = potentialVariableMap.getVariableById(id);\n }\n if (variable) {\n return variable;\n }\n }\n // If there was no ID, or there was an ID but it didn't match any variables,\n // look up by name and type.\n if (opt_name) {\n if (opt_type === undefined) {\n throw Error('Tried to look up a variable by name without a type');\n }\n // Otherwise look up by name and type.\n variable = workspace.getVariable(opt_name, opt_type);\n if (!variable && potentialVariableMap) {\n variable = potentialVariableMap.getVariable(opt_name, opt_type);\n }\n }\n return variable;\n}\n\n/**\n * Helper function to create a variable on the given workspace.\n *\n * @param workspace The workspace in which to create the variable. It may be a\n * flyout workspace or main workspace.\n * @param id The ID to use to create the variable, or null.\n * @param opt_name The string to use to create the variable.\n * @param opt_type The type to use to create the variable.\n * @returns The variable corresponding to the given ID or name + type\n * combination.\n */\nfunction createVariable(\n workspace: Workspace, id: string|null, opt_name?: string,\n opt_type?: string): VariableModel {\n const potentialVariableMap = workspace.getPotentialVariableMap();\n // Variables without names get uniquely named for this workspace.\n if (!opt_name) {\n const ws =\n (workspace.isFlyout ? (workspace as WorkspaceSvg).targetWorkspace :\n workspace);\n opt_name = generateUniqueName(ws!);\n }\n\n // Create a potential variable if in the flyout.\n let variable = null;\n if (potentialVariableMap) {\n variable = potentialVariableMap.createVariable(opt_name, opt_type, id);\n } else {\n // In the main workspace, create a real variable.\n variable = workspace.createVariable(opt_name, opt_type, id);\n }\n return variable;\n}\n\n/**\n * Helper function to get the list of variables that have been added to the\n * workspace after adding a new block, using the given list of variables that\n * were in the workspace before the new block was added.\n *\n * @param workspace The workspace to inspect.\n * @param originalVariables The array of variables that existed in the workspace\n * before adding the new block.\n * @returns The new array of variables that were freshly added to the workspace\n * after creating the new block, or [] if no new variables were added to the\n * workspace.\n * @alias Blockly.Variables.getAddedVariables\n * @internal\n */\nexport function getAddedVariables(\n workspace: Workspace, originalVariables: VariableModel[]): VariableModel[] {\n const allCurrentVariables = workspace.getAllVariables();\n const addedVariables = [];\n if (originalVariables.length !== allCurrentVariables.length) {\n for (let i = 0; i < allCurrentVariables.length; i++) {\n const variable = allCurrentVariables[i];\n // For any variable that is present in allCurrentVariables but not\n // present in originalVariables, add the variable to addedVariables.\n if (originalVariables.indexOf(variable) === -1) {\n addedVariables.push(variable);\n }\n }\n }\n return addedVariables;\n}\n\nexport const TEST_ONLY = {\n generateUniqueNameInternal,\n};\n","/**\n * @license\n * Copyright 2013 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Inject Blockly's CSS synchronously.\n *\n * @namespace Blockly.Css\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Css');\n\n\n/** Has CSS already been injected? */\nlet injected = false;\n\n/**\n * Add some CSS to the blob that will be injected later. Allows optional\n * components such as fields and the toolbox to store separate CSS.\n *\n * @param cssContent Multiline CSS string or an array of single lines of CSS.\n * @alias Blockly.Css.register\n */\nexport function register(cssContent: string) {\n if (injected) {\n throw Error('CSS already injected');\n }\n content += '\\n' + cssContent;\n}\n\n/**\n * Inject the CSS into the DOM. This is preferable over using a regular CSS\n * file since:\n * a) It loads synchronously and doesn't force a redraw later.\n * b) It speeds up loading by not blocking on a separate HTTP transfer.\n * c) The CSS content may be made dynamic depending on init options.\n *\n * @param hasCss If false, don't inject CSS (providing CSS becomes the\n * document's responsibility).\n * @param pathToMedia Path from page to the Blockly media directory.\n * @alias Blockly.Css.inject\n */\nexport function inject(hasCss: boolean, pathToMedia: string) {\n // Only inject the CSS once.\n if (injected) {\n return;\n }\n injected = true;\n if (!hasCss) {\n return;\n }\n // Strip off any trailing slash (either Unix or Windows).\n const mediaPath = pathToMedia.replace(/[\\\\/]$/, '');\n const cssContent = content.replace(/<<>>/g, mediaPath);\n // Cleanup the collected css content after injecting it to the DOM.\n content = '';\n\n // Inject CSS tag at start of head.\n const cssNode = document.createElement('style');\n cssNode.id = 'blockly-common-style';\n const cssTextNode = document.createTextNode(cssContent);\n cssNode.appendChild(cssTextNode);\n document.head.insertBefore(cssNode, document.head.firstChild);\n}\n\n/**\n * The CSS content for Blockly.\n *\n * @alias Blockly.Css.content\n */\nlet content = `\n.blocklySvg {\n background-color: #fff;\n outline: none;\n overflow: hidden; /* IE overflows by default. */\n position: absolute;\n display: block;\n}\n\n.blocklyWidgetDiv {\n display: none;\n position: absolute;\n z-index: 99999; /* big value for bootstrap3 compatibility */\n}\n\n.injectionDiv {\n height: 100%;\n position: relative;\n overflow: hidden; /* So blocks in drag surface disappear at edges */\n touch-action: none;\n}\n\n.blocklyNonSelectable {\n user-select: none;\n -ms-user-select: none;\n -webkit-user-select: none;\n}\n\n.blocklyWsDragSurface {\n display: none;\n position: absolute;\n top: 0;\n left: 0;\n}\n\n/* Added as a separate rule with multiple classes to make it more specific\n than a bootstrap rule that selects svg:root. See issue #1275 for context.\n*/\n.blocklyWsDragSurface.blocklyOverflowVisible {\n overflow: visible;\n}\n\n.blocklyBlockDragSurface {\n display: none;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n overflow: visible !important;\n z-index: 50; /* Display below toolbox, but above everything else. */\n}\n\n.blocklyBlockCanvas.blocklyCanvasTransitioning,\n.blocklyBubbleCanvas.blocklyCanvasTransitioning {\n transition: transform .5s;\n}\n\n.blocklyTooltipDiv {\n background-color: #ffffc7;\n border: 1px solid #ddc;\n box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);\n color: #000;\n display: none;\n font: 9pt sans-serif;\n opacity: .9;\n padding: 2px;\n position: absolute;\n z-index: 100000; /* big value for bootstrap3 compatibility */\n}\n\n.blocklyDropDownDiv {\n position: absolute;\n left: 0;\n top: 0;\n z-index: 1000;\n display: none;\n border: 1px solid;\n border-color: #dadce0;\n background-color: #fff;\n border-radius: 2px;\n padding: 4px;\n box-shadow: 0 0 3px 1px rgba(0,0,0,.3);\n}\n\n.blocklyDropDownDiv.blocklyFocused {\n box-shadow: 0 0 6px 1px rgba(0,0,0,.3);\n}\n\n.blocklyDropDownContent {\n max-height: 300px; // @todo: spec for maximum height.\n overflow: auto;\n overflow-x: hidden;\n position: relative;\n}\n\n.blocklyDropDownArrow {\n position: absolute;\n left: 0;\n top: 0;\n width: 16px;\n height: 16px;\n z-index: -1;\n background-color: inherit;\n border-color: inherit;\n}\n\n.blocklyDropDownButton {\n display: inline-block;\n float: left;\n padding: 0;\n margin: 4px;\n border-radius: 4px;\n outline: none;\n border: 1px solid;\n transition: box-shadow .1s;\n cursor: pointer;\n}\n\n.blocklyArrowTop {\n border-top: 1px solid;\n border-left: 1px solid;\n border-top-left-radius: 4px;\n border-color: inherit;\n}\n\n.blocklyArrowBottom {\n border-bottom: 1px solid;\n border-right: 1px solid;\n border-bottom-right-radius: 4px;\n border-color: inherit;\n}\n\n.blocklyResizeSE {\n cursor: se-resize;\n fill: #aaa;\n}\n\n.blocklyResizeSW {\n cursor: sw-resize;\n fill: #aaa;\n}\n\n.blocklyResizeLine {\n stroke: #515A5A;\n stroke-width: 1;\n}\n\n.blocklyHighlightedConnectionPath {\n fill: none;\n stroke: #fc3;\n stroke-width: 4px;\n}\n\n.blocklyPathLight {\n fill: none;\n stroke-linecap: round;\n stroke-width: 1;\n}\n\n.blocklySelected>.blocklyPathLight {\n display: none;\n}\n\n.blocklyDraggable {\n /* backup for browsers (e.g. IE11) that don't support grab */\n cursor: url(\"<<>>/handopen.cur\"), auto;\n cursor: grab;\n cursor: -webkit-grab;\n}\n\n /* backup for browsers (e.g. IE11) that don't support grabbing */\n.blocklyDragging {\n /* backup for browsers (e.g. IE11) that don't support grabbing */\n cursor: url(\"<<>>/handclosed.cur\"), auto;\n cursor: grabbing;\n cursor: -webkit-grabbing;\n}\n\n /* Changes cursor on mouse down. Not effective in Firefox because of\n https://bugzilla.mozilla.org/show_bug.cgi?id=771241 */\n.blocklyDraggable:active {\n /* backup for browsers (e.g. IE11) that don't support grabbing */\n cursor: url(\"<<>>/handclosed.cur\"), auto;\n cursor: grabbing;\n cursor: -webkit-grabbing;\n}\n\n/* Change the cursor on the whole drag surface in case the mouse gets\n ahead of block during a drag. This way the cursor is still a closed hand.\n */\n.blocklyBlockDragSurface .blocklyDraggable {\n /* backup for browsers (e.g. IE11) that don't support grabbing */\n cursor: url(\"<<>>/handclosed.cur\"), auto;\n cursor: grabbing;\n cursor: -webkit-grabbing;\n}\n\n.blocklyDragging.blocklyDraggingDelete {\n cursor: url(\"<<>>/handdelete.cur\"), auto;\n}\n\n.blocklyDragging>.blocklyPath,\n.blocklyDragging>.blocklyPathLight {\n fill-opacity: .8;\n stroke-opacity: .8;\n}\n\n.blocklyDragging>.blocklyPathDark {\n display: none;\n}\n\n.blocklyDisabled>.blocklyPath {\n fill-opacity: .5;\n stroke-opacity: .5;\n}\n\n.blocklyDisabled>.blocklyPathLight,\n.blocklyDisabled>.blocklyPathDark {\n display: none;\n}\n\n.blocklyInsertionMarker>.blocklyPath,\n.blocklyInsertionMarker>.blocklyPathLight,\n.blocklyInsertionMarker>.blocklyPathDark {\n fill-opacity: .2;\n stroke: none;\n}\n\n.blocklyMultilineText {\n font-family: monospace;\n}\n\n.blocklyNonEditableText>text {\n pointer-events: none;\n}\n\n.blocklyFlyout {\n position: absolute;\n z-index: 20;\n}\n\n.blocklyText text {\n cursor: default;\n}\n\n/*\n Don't allow users to select text. It gets annoying when trying to\n drag a block and selected text moves instead.\n*/\n.blocklySvg text,\n.blocklyBlockDragSurface text {\n user-select: none;\n -ms-user-select: none;\n -webkit-user-select: none;\n cursor: inherit;\n}\n\n.blocklyHidden {\n display: none;\n}\n\n.blocklyFieldDropdown:not(.blocklyHidden) {\n display: block;\n}\n\n.blocklyIconGroup {\n cursor: default;\n}\n\n.blocklyIconGroup:not(:hover),\n.blocklyIconGroupReadonly {\n opacity: .6;\n}\n\n.blocklyIconShape {\n fill: #00f;\n stroke: #fff;\n stroke-width: 1px;\n}\n\n.blocklyIconSymbol {\n fill: #fff;\n}\n\n.blocklyMinimalBody {\n margin: 0;\n padding: 0;\n}\n\n.blocklyHtmlInput {\n border: none;\n border-radius: 4px;\n height: 100%;\n margin: 0;\n outline: none;\n padding: 0;\n width: 100%;\n text-align: center;\n display: block;\n box-sizing: border-box;\n}\n\n/* Edge and IE introduce a close icon when the input value is longer than a\n certain length. This affects our sizing calculations of the text input.\n Hiding the close icon to avoid that. */\n.blocklyHtmlInput::-ms-clear {\n display: none;\n}\n\n.blocklyMainBackground {\n stroke-width: 1;\n stroke: #c6c6c6; /* Equates to #ddd due to border being off-pixel. */\n}\n\n.blocklyMutatorBackground {\n fill: #fff;\n stroke: #ddd;\n stroke-width: 1;\n}\n\n.blocklyFlyoutBackground {\n fill: #ddd;\n fill-opacity: .8;\n}\n\n.blocklyMainWorkspaceScrollbar {\n z-index: 20;\n}\n\n.blocklyFlyoutScrollbar {\n z-index: 30;\n}\n\n.blocklyScrollbarHorizontal,\n.blocklyScrollbarVertical {\n position: absolute;\n outline: none;\n}\n\n.blocklyScrollbarBackground {\n opacity: 0;\n}\n\n.blocklyScrollbarHandle {\n fill: #ccc;\n}\n\n.blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,\n.blocklyScrollbarHandle:hover {\n fill: #bbb;\n}\n\n/* Darken flyout scrollbars due to being on a grey background. */\n/* By contrast, workspace scrollbars are on a white background. */\n.blocklyFlyout .blocklyScrollbarHandle {\n fill: #bbb;\n}\n\n.blocklyFlyout .blocklyScrollbarBackground:hover+.blocklyScrollbarHandle,\n.blocklyFlyout .blocklyScrollbarHandle:hover {\n fill: #aaa;\n}\n\n.blocklyInvalidInput {\n background: #faa;\n}\n\n.blocklyVerticalMarker {\n stroke-width: 3px;\n fill: rgba(255,255,255,.5);\n pointer-events: none;\n}\n\n.blocklyComputeCanvas {\n position: absolute;\n width: 0;\n height: 0;\n}\n\n.blocklyNoPointerEvents {\n pointer-events: none;\n}\n\n.blocklyContextMenu {\n border-radius: 4px;\n max-height: 100%;\n}\n\n.blocklyDropdownMenu {\n border-radius: 2px;\n padding: 0 !important;\n}\n\n.blocklyDropdownMenu .blocklyMenuItem {\n /* 28px on the left for icon or checkbox. */\n padding-left: 28px;\n}\n\n/* BiDi override for the resting state. */\n.blocklyDropdownMenu .blocklyMenuItemRtl {\n /* Flip left/right padding for BiDi. */\n padding-left: 5px;\n padding-right: 28px;\n}\n\n.blocklyWidgetDiv .blocklyMenu {\n background: #fff;\n border: 1px solid transparent;\n box-shadow: 0 0 3px 1px rgba(0,0,0,.3);\n font: normal 13px Arial, sans-serif;\n margin: 0;\n outline: none;\n padding: 4px 0;\n position: absolute;\n overflow-y: auto;\n overflow-x: hidden;\n max-height: 100%;\n z-index: 20000; /* Arbitrary, but some apps depend on it... */\n}\n\n.blocklyWidgetDiv .blocklyMenu.blocklyFocused {\n box-shadow: 0 0 6px 1px rgba(0,0,0,.3);\n}\n\n.blocklyDropDownDiv .blocklyMenu {\n background: inherit; /* Compatibility with gapi, reset from goog-menu */\n border: inherit; /* Compatibility with gapi, reset from goog-menu */\n font: normal 13px \"Helvetica Neue\", Helvetica, sans-serif;\n outline: none;\n position: relative; /* Compatibility with gapi, reset from goog-menu */\n z-index: 20000; /* Arbitrary, but some apps depend on it... */\n}\n\n/* State: resting. */\n.blocklyMenuItem {\n border: none;\n color: #000;\n cursor: pointer;\n list-style: none;\n margin: 0;\n /* 7em on the right for shortcut. */\n min-width: 7em;\n padding: 6px 15px;\n white-space: nowrap;\n}\n\n/* State: disabled. */\n.blocklyMenuItemDisabled {\n color: #ccc;\n cursor: inherit;\n}\n\n/* State: hover. */\n.blocklyMenuItemHighlight {\n background-color: rgba(0,0,0,.1);\n}\n\n/* State: selected/checked. */\n.blocklyMenuItemCheckbox {\n height: 16px;\n position: absolute;\n width: 16px;\n}\n\n.blocklyMenuItemSelected .blocklyMenuItemCheckbox {\n background: url(<<>>/sprites.png) no-repeat -48px -16px;\n float: left;\n margin-left: -24px;\n position: static; /* Scroll with the menu. */\n}\n\n.blocklyMenuItemRtl .blocklyMenuItemCheckbox {\n float: right;\n margin-right: -24px;\n}\n`;\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods realted to figuring out positions of SVG elements.\n *\n * @namespace Blockly.utils.svgMath\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.svgMath');\n\nimport type {WorkspaceSvg} from '../workspace_svg.js';\n\nimport {Coordinate} from './coordinate.js';\nimport * as deprecation from './deprecation.js';\nimport {Rect} from './rect.js';\nimport * as style from './style.js';\n\n\n/**\n * Static regex to pull the x,y values out of an SVG translate() directive.\n * Note that Firefox and IE (9,10) return 'translate(12)' instead of\n * 'translate(12, 0)'.\n * Note that IE (9,10) returns 'translate(16 8)' instead of 'translate(16, 8)'.\n * Note that IE has been reported to return scientific notation (0.123456e-42).\n */\nconst XY_REGEX = /translate\\(\\s*([-+\\d.e]+)([ ,]\\s*([-+\\d.e]+)\\s*)?/;\n\n/**\n * Static regex to pull the x,y values out of a translate() or translate3d()\n * style property.\n * Accounts for same exceptions as XY_REGEX.\n */\nconst XY_STYLE_REGEX =\n /transform:\\s*translate(?:3d)?\\(\\s*([-+\\d.e]+)\\s*px([ ,]\\s*([-+\\d.e]+)\\s*px)?/;\n\n/**\n * Return the coordinates of the top-left corner of this element relative to\n * its parent. Only for SVG elements and children (e.g. rect, g, path).\n *\n * @param element SVG element to find the coordinates of.\n * @returns Object with .x and .y properties.\n * @alias Blockly.utils.svgMath.getRelativeXY\n */\nexport function getRelativeXY(element: Element): Coordinate {\n const xy = new Coordinate(0, 0);\n // First, check for x and y attributes.\n // Checking for the existence of x/y properties is faster than getAttribute.\n // However, x/y contains an SVGAnimatedLength object, so rely on getAttribute\n // to get the number.\n const x = (element as any).x && element.getAttribute('x');\n const y = (element as any).y && element.getAttribute('y');\n if (x) {\n xy.x = parseInt(x);\n }\n if (y) {\n xy.y = parseInt(y);\n }\n // Second, check for transform=\"translate(...)\" attribute.\n const transform = element.getAttribute('transform');\n const r = transform && transform.match(XY_REGEX);\n if (r) {\n xy.x += Number(r[1]);\n if (r[3]) {\n xy.y += Number(r[3]);\n }\n }\n\n // Then check for style = transform: translate(...) or translate3d(...)\n const style = element.getAttribute('style');\n if (style && style.indexOf('translate') > -1) {\n const styleComponents = style.match(XY_STYLE_REGEX);\n if (styleComponents) {\n xy.x += Number(styleComponents[1]);\n if (styleComponents[3]) {\n xy.y += Number(styleComponents[3]);\n }\n }\n }\n return xy;\n}\n\n/**\n * Return the coordinates of the top-left corner of this element relative to\n * the div Blockly was injected into.\n *\n * @param element SVG element to find the coordinates of. If this is not a child\n * of the div Blockly was injected into, the behaviour is undefined.\n * @returns Object with .x and .y properties.\n * @alias Blockly.utils.svgMath.getInjectionDivXY\n */\nexport function getInjectionDivXY(element: Element): Coordinate {\n let x = 0;\n let y = 0;\n while (element) {\n const xy = getRelativeXY(element);\n x = x + xy.x;\n y = y + xy.y;\n const classes = element.getAttribute('class') || '';\n if ((' ' + classes + ' ').indexOf(' injectionDiv ') !== -1) {\n break;\n }\n element = element.parentNode as Element;\n }\n return new Coordinate(x, y);\n}\n\n/**\n * Check if 3D transforms are supported by adding an element\n * and attempting to set the property.\n *\n * @returns True if 3D transforms are supported.\n * @deprecated No longer provided by Blockly.\n * @alias Blockly.utils.svgMath.is3dSupported\n */\nexport function is3dSupported(): boolean {\n // All browsers support translate3d in 2022.\n deprecation.warn(\n 'Blockly.utils.svgMath.is3dSupported', 'version 9', 'version 10');\n return true;\n}\n\n/**\n * Get the position of the current viewport in window coordinates. This takes\n * scroll into account.\n *\n * @returns An object containing window width, height, and scroll position in\n * window coordinates.\n * @alias Blockly.utils.svgMath.getViewportBBox\n * @internal\n */\nexport function getViewportBBox(): Rect {\n // Pixels, in window coordinates.\n const scrollOffset = style.getViewportPageOffset();\n return new Rect(\n scrollOffset.y, document.documentElement.clientHeight + scrollOffset.y,\n scrollOffset.x, document.documentElement.clientWidth + scrollOffset.x);\n}\n\n/**\n * Gets the document scroll distance as a coordinate object.\n * Copied from Closure's goog.dom.getDocumentScroll.\n *\n * @returns Object with values 'x' and 'y'.\n * @alias Blockly.utils.svgMath.getDocumentScroll\n */\nexport function getDocumentScroll(): Coordinate {\n const el = document.documentElement;\n const win = window;\n return new Coordinate(\n win.pageXOffset || el.scrollLeft, win.pageYOffset || el.scrollTop);\n}\n\n/**\n * Converts screen coordinates to workspace coordinates.\n *\n * @param ws The workspace to find the coordinates on.\n * @param screenCoordinates The screen coordinates to be converted to workspace\n * coordinates\n * @returns The workspace coordinates.\n * @alias Blockly.utils.svgMath.screenToWsCoordinates\n */\nexport function screenToWsCoordinates(\n ws: WorkspaceSvg, screenCoordinates: Coordinate): Coordinate {\n const screenX = screenCoordinates.x;\n const screenY = screenCoordinates.y;\n\n const injectionDiv = ws.getInjectionDiv();\n // Bounding rect coordinates are in client coordinates, meaning that they\n // are in pixels relative to the upper left corner of the visible browser\n // window. These coordinates change when you scroll the browser window.\n const boundingRect = injectionDiv.getBoundingClientRect();\n\n // The client coordinates offset by the injection div's upper left corner.\n const clientOffsetPixels =\n new Coordinate(screenX - boundingRect.left, screenY - boundingRect.top);\n\n // The offset in pixels between the main workspace's origin and the upper\n // left corner of the injection div.\n const mainOffsetPixels = ws.getOriginOffsetInPixels();\n\n // The position of the new comment in pixels relative to the origin of the\n // main workspace.\n const finalOffsetPixels =\n Coordinate.difference(clientOffsetPixels, mainOffsetPixels);\n // The position in main workspace coordinates.\n const finalOffsetMainWs = finalOffsetPixels.scale(1 / ws.scale);\n return finalOffsetMainWs;\n}\n\nexport const TEST_ONLY = {\n XY_REGEX,\n XY_STYLE_REGEX,\n};\n","/**\n * @license\n * Copyright 2012 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * XML reader and writer.\n *\n * @namespace Blockly.Xml\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Xml');\n\nimport type {Block} from './block.js';\nimport type {BlockSvg} from './block_svg.js';\nimport type {Connection} from './connection.js';\nimport * as eventUtils from './events/utils.js';\nimport type {Field} from './field.js';\nimport {inputTypes} from './input_types.js';\nimport * as dom from './utils/dom.js';\nimport {Size} from './utils/size.js';\nimport * as utilsXml from './utils/xml.js';\nimport type {VariableModel} from './variable_model.js';\nimport * as Variables from './variables.js';\nimport type {Workspace} from './workspace.js';\nimport {WorkspaceComment} from './workspace_comment.js';\nimport {WorkspaceCommentSvg} from './workspace_comment_svg.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\n/**\n * Encode a block tree as XML.\n *\n * @param workspace The workspace containing blocks.\n * @param opt_noId True if the encoder should skip the block IDs.\n * @returns XML DOM element.\n * @alias Blockly.Xml.workspaceToDom\n */\nexport function workspaceToDom(\n workspace: Workspace, opt_noId?: boolean): Element {\n const treeXml = utilsXml.createElement('xml');\n const variablesElement =\n variablesToDom(Variables.allUsedVarModels(workspace));\n if (variablesElement.hasChildNodes()) {\n treeXml.appendChild(variablesElement);\n }\n const comments = workspace.getTopComments(true);\n for (let i = 0; i < comments.length; i++) {\n const comment = comments[i];\n treeXml.appendChild(comment.toXmlWithXY(opt_noId));\n }\n const blocks = workspace.getTopBlocks(true);\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i];\n treeXml.appendChild(blockToDomWithXY(block, opt_noId));\n }\n return treeXml;\n}\n\n/**\n * Encode a list of variables as XML.\n *\n * @param variableList List of all variable models.\n * @returns Tree of XML elements.\n * @alias Blockly.Xml.variablesToDom\n */\nexport function variablesToDom(variableList: VariableModel[]): Element {\n const variables = utilsXml.createElement('variables');\n for (let i = 0; i < variableList.length; i++) {\n const variable = variableList[i];\n const element = utilsXml.createElement('variable');\n element.appendChild(utilsXml.createTextNode(variable.name));\n if (variable.type) {\n element.setAttribute('type', variable.type);\n }\n element.id = variable.getId();\n variables.appendChild(element);\n }\n return variables;\n}\n\n/**\n * Encode a block subtree as XML with XY coordinates.\n *\n * @param block The root block to encode.\n * @param opt_noId True if the encoder should skip the block ID.\n * @returns Tree of XML elements or an empty document fragment if the block was\n * an insertion marker.\n * @alias Blockly.Xml.blockToDomWithXY\n */\nexport function blockToDomWithXY(block: Block, opt_noId?: boolean): Element|\n DocumentFragment {\n if (block.isInsertionMarker()) { // Skip over insertion markers.\n block = block.getChildren(false)[0];\n if (!block) {\n // Disappears when appended.\n return new DocumentFragment();\n }\n }\n\n let width = 0; // Not used in LTR.\n if (block.workspace.RTL) {\n width = block.workspace.getWidth();\n }\n\n const element = blockToDom(block, opt_noId);\n const xy = block.getRelativeToSurfaceXY();\n // AnyDuringMigration because: Property 'setAttribute' does not exist on type\n // 'Element | DocumentFragment'.\n (element as AnyDuringMigration)\n .setAttribute('x', Math.round(block.workspace.RTL ? width - xy.x : xy.x));\n // AnyDuringMigration because: Property 'setAttribute' does not exist on type\n // 'Element | DocumentFragment'.\n (element as AnyDuringMigration).setAttribute('y', Math.round(xy.y));\n return element;\n}\n\n/**\n * Encode a field as XML.\n *\n * @param field The field to encode.\n * @returns XML element, or null if the field did not need to be serialized.\n */\nfunction fieldToDom(field: Field): Element|null {\n if (field.isSerializable()) {\n const container = utilsXml.createElement('field');\n container.setAttribute('name', field.name || '');\n return field.toXml(container);\n }\n return null;\n}\n\n/**\n * Encode all of a block's fields as XML and attach them to the given tree of\n * XML elements.\n *\n * @param block A block with fields to be encoded.\n * @param element The XML element to which the field DOM should be attached.\n */\nfunction allFieldsToDom(block: Block, element: Element) {\n for (let i = 0; i < block.inputList.length; i++) {\n const input = block.inputList[i];\n for (let j = 0; j < input.fieldRow.length; j++) {\n const field = input.fieldRow[j];\n const fieldDom = fieldToDom(field);\n if (fieldDom) {\n element.appendChild(fieldDom);\n }\n }\n }\n}\n\n/**\n * Encode a block subtree as XML.\n *\n * @param block The root block to encode.\n * @param opt_noId True if the encoder should skip the block ID.\n * @returns Tree of XML elements or an empty document fragment if the block was\n * an insertion marker.\n * @alias Blockly.Xml.blockToDom\n */\nexport function blockToDom(block: Block, opt_noId?: boolean): Element|\n DocumentFragment {\n // Skip over insertion markers.\n if (block.isInsertionMarker()) {\n const child = block.getChildren(false)[0];\n if (child) {\n return blockToDom(child);\n } else {\n // Disappears when appended.\n return new DocumentFragment();\n }\n }\n\n const element = utilsXml.createElement(block.isShadow() ? 'shadow' : 'block');\n element.setAttribute('type', block.type);\n if (!opt_noId) {\n // It's important to use setAttribute here otherwise IE11 won't serialize\n // the block's ID when domToText is called.\n element.setAttribute('id', block.id);\n }\n if (block.mutationToDom) {\n // Custom data for an advanced block.\n const mutation = block.mutationToDom();\n if (mutation && (mutation.hasChildNodes() || mutation.hasAttributes())) {\n element.appendChild(mutation);\n }\n }\n\n allFieldsToDom(block, element);\n\n const commentText = block.getCommentText();\n if (commentText) {\n const size = block.commentModel.size;\n const pinned = block.commentModel.pinned;\n\n const commentElement = utilsXml.createElement('comment');\n commentElement.appendChild(utilsXml.createTextNode(commentText));\n // AnyDuringMigration because: Argument of type 'boolean' is not assignable\n // to parameter of type 'string'.\n commentElement.setAttribute('pinned', pinned as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type 'number' is not assignable\n // to parameter of type 'string'.\n commentElement.setAttribute('h', size.height as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type 'number' is not assignable\n // to parameter of type 'string'.\n commentElement.setAttribute('w', size.width as AnyDuringMigration);\n\n element.appendChild(commentElement);\n }\n\n if (block.data) {\n const dataElement = utilsXml.createElement('data');\n dataElement.appendChild(utilsXml.createTextNode(block.data));\n element.appendChild(dataElement);\n }\n\n for (let i = 0; i < block.inputList.length; i++) {\n const input = block.inputList[i];\n let container: Element;\n let empty = true;\n if (input.type === inputTypes.DUMMY) {\n continue;\n } else {\n const childBlock = input.connection!.targetBlock();\n if (input.type === inputTypes.VALUE) {\n container = utilsXml.createElement('value');\n } else if (input.type === inputTypes.STATEMENT) {\n container = utilsXml.createElement('statement');\n }\n const childShadow = input.connection!.getShadowDom();\n if (childShadow && (!childBlock || !childBlock.isShadow())) {\n container!.appendChild(cloneShadow(childShadow, opt_noId));\n }\n if (childBlock) {\n const childElem = blockToDom(childBlock, opt_noId);\n if (childElem.nodeType === dom.NodeType.ELEMENT_NODE) {\n container!.appendChild(childElem);\n empty = false;\n }\n }\n }\n container!.setAttribute('name', input.name);\n if (!empty) {\n element.appendChild(container!);\n }\n }\n if (block.inputsInline !== undefined &&\n block.inputsInline !== block.inputsInlineDefault) {\n element.setAttribute('inline', block.inputsInline.toString());\n }\n if (block.isCollapsed()) {\n element.setAttribute('collapsed', 'true');\n }\n if (!block.isEnabled()) {\n element.setAttribute('disabled', 'true');\n }\n if (!block.isDeletable() && !block.isShadow()) {\n element.setAttribute('deletable', 'false');\n }\n if (!block.isMovable() && !block.isShadow()) {\n element.setAttribute('movable', 'false');\n }\n if (!block.isEditable()) {\n element.setAttribute('editable', 'false');\n }\n\n const nextBlock = block.getNextBlock();\n let container: Element;\n if (nextBlock) {\n const nextElem = blockToDom(nextBlock, opt_noId);\n if (nextElem.nodeType === dom.NodeType.ELEMENT_NODE) {\n container = utilsXml.createElement('next');\n container.appendChild(nextElem);\n element.appendChild(container);\n }\n }\n const nextShadow =\n block.nextConnection && block.nextConnection.getShadowDom();\n if (nextShadow && (!nextBlock || !nextBlock.isShadow())) {\n container!.appendChild(cloneShadow(nextShadow, opt_noId));\n }\n\n return element;\n}\n\n/**\n * Deeply clone the shadow's DOM so that changes don't back-wash to the block.\n *\n * @param shadow A tree of XML elements.\n * @param opt_noId True if the encoder should skip the block ID.\n * @returns A tree of XML elements.\n */\nfunction cloneShadow(shadow: Element, opt_noId?: boolean): Element {\n shadow = shadow.cloneNode(true) as Element;\n // Walk the tree looking for whitespace. Don't prune whitespace in a tag.\n let node: Node|null = shadow;\n let textNode;\n while (node) {\n if (opt_noId && node.nodeName === 'shadow') {\n // Strip off IDs from shadow blocks. There should never be a 'block' as\n // a child of a 'shadow', so no need to check that.\n (node as Element).removeAttribute('id');\n }\n if (node.firstChild) {\n node = node.firstChild;\n } else {\n while (node && !node.nextSibling) {\n textNode = node;\n node = node.parentNode;\n if (textNode.nodeType === dom.NodeType.TEXT_NODE &&\n (textNode as Text).data.trim() === '' &&\n node?.firstChild !== textNode) {\n // Prune whitespace after a tag.\n dom.removeNode(textNode);\n }\n }\n if (node) {\n textNode = node;\n node = node.nextSibling;\n if (textNode.nodeType === dom.NodeType.TEXT_NODE &&\n (textNode as Text).data.trim() === '') {\n // Prune whitespace before a tag.\n dom.removeNode(textNode);\n }\n }\n }\n }\n return shadow;\n}\n\n/**\n * Converts a DOM structure into plain text.\n * Currently the text format is fairly ugly: all one line with no whitespace,\n * unless the DOM itself has whitespace built-in.\n *\n * @param dom A tree of XML nodes.\n * @returns Text representation.\n * @alias Blockly.Xml.domToText\n */\nexport function domToText(dom: Node): string {\n const text = utilsXml.domToText(dom);\n // Unpack self-closing tags. These tags fail when embedded in HTML.\n // -> \n return text.replace(/<(\\w+)([^<]*)\\/>/g, '<$1$2>');\n}\n\n/**\n * Converts a DOM structure into properly indented text.\n *\n * @param dom A tree of XML elements.\n * @returns Text representation.\n * @alias Blockly.Xml.domToPrettyText\n */\nexport function domToPrettyText(dom: Node): string {\n // This function is not guaranteed to be correct for all XML.\n // But it handles the XML that Blockly generates.\n const blob = domToText(dom);\n // Place every open and close tag on its own line.\n const lines = blob.split('<');\n // Indent every line.\n let indent = '';\n for (let i = 1; i < lines.length; i++) {\n const line = lines[i];\n if (line[0] === '/') {\n indent = indent.substring(2);\n }\n lines[i] = indent + '<' + line;\n if (line[0] !== '/' && line.slice(-2) !== '/>') {\n indent += ' ';\n }\n }\n // Pull simple tags back together.\n // E.g. \n let text = lines.join('\\n');\n text = text.replace(/(<(\\w+)\\b[^>]*>[^\\n]*)\\n *<\\/\\2>/g, '$1');\n // Trim leading blank line.\n return text.replace(/^\\n/, '');\n}\n\n/**\n * Converts an XML string into a DOM structure.\n *\n * @param text An XML string.\n * @returns A DOM object representing the singular child of the document\n * element.\n * @throws if the text doesn't parse.\n * @alias Blockly.Xml.textToDom\n */\nexport function textToDom(text: string): Element {\n const doc = utilsXml.textToDomDocument(text);\n if (!doc || !doc.documentElement ||\n doc.getElementsByTagName('parsererror').length) {\n throw Error('textToDom was unable to parse: ' + text);\n }\n return doc.documentElement;\n}\n\n/**\n * Clear the given workspace then decode an XML DOM and\n * create blocks on the workspace.\n *\n * @param xml XML DOM.\n * @param workspace The workspace.\n * @returns An array containing new block IDs.\n * @alias Blockly.Xml.clearWorkspaceAndLoadFromXml\n */\nexport function clearWorkspaceAndLoadFromXml(\n xml: Element, workspace: WorkspaceSvg): string[] {\n workspace.setResizesEnabled(false);\n workspace.clear();\n // AnyDuringMigration because: Argument of type 'WorkspaceSvg' is not\n // assignable to parameter of type 'Workspace'.\n const blockIds = domToWorkspace(xml, workspace as AnyDuringMigration);\n workspace.setResizesEnabled(true);\n return blockIds;\n}\n\n/**\n * Decode an XML DOM and create blocks on the workspace.\n *\n * @param xml XML DOM.\n * @param workspace The workspace.\n * @returns An array containing new block IDs.\n * @suppress {strictModuleDepCheck} Suppress module check while workspace\n * comments are not bundled in.\n * @alias Blockly.Xml.domToWorkspace\n */\nexport function domToWorkspace(xml: Element, workspace: Workspace): string[] {\n let width = 0; // Not used in LTR.\n if (workspace.RTL) {\n width = workspace.getWidth();\n }\n const newBlockIds = []; // A list of block IDs added by this call.\n dom.startTextWidthCache();\n const existingGroup = eventUtils.getGroup();\n if (!existingGroup) {\n eventUtils.setGroup(true);\n }\n\n // Disable workspace resizes as an optimization.\n // Assume it is rendered so we can check.\n if ((workspace as WorkspaceSvg).setResizesEnabled) {\n (workspace as WorkspaceSvg).setResizesEnabled(false);\n }\n let variablesFirst = true;\n try {\n for (let i = 0, xmlChild; xmlChild = xml.childNodes[i]; i++) {\n const name = xmlChild.nodeName.toLowerCase();\n const xmlChildElement = xmlChild as Element;\n if (name === 'block' ||\n name === 'shadow' && !eventUtils.getRecordUndo()) {\n // Allow top-level shadow blocks if recordUndo is disabled since\n // that means an undo is in progress. Such a block is expected\n // to be moved to a nested destination in the next operation.\n const block = domToBlock(xmlChildElement, workspace);\n newBlockIds.push(block.id);\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n const blockX = xmlChildElement.hasAttribute('x') ?\n parseInt(xmlChildElement.getAttribute('x') as AnyDuringMigration) :\n 10;\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n const blockY = xmlChildElement.hasAttribute('y') ?\n parseInt(xmlChildElement.getAttribute('y') as AnyDuringMigration) :\n 10;\n if (!isNaN(blockX) && !isNaN(blockY)) {\n block.moveBy(workspace.RTL ? width - blockX : blockX, blockY);\n }\n variablesFirst = false;\n } else if (name === 'shadow') {\n throw TypeError('Shadow block cannot be a top-level block.');\n } else if (name === 'comment') {\n if (workspace.rendered) {\n WorkspaceCommentSvg.fromXmlRendered(\n xmlChildElement, workspace as WorkspaceSvg, width);\n } else {\n WorkspaceComment.fromXml(xmlChildElement, workspace);\n }\n } else if (name === 'variables') {\n if (variablesFirst) {\n domToVariables(xmlChildElement, workspace);\n } else {\n throw Error(\n '\\'variables\\' tag must exist once before block and ' +\n 'shadow tag elements in the workspace XML, but it was found in ' +\n 'another location.');\n }\n variablesFirst = false;\n }\n }\n } finally {\n if (!existingGroup) {\n eventUtils.setGroup(false);\n }\n dom.stopTextWidthCache();\n }\n // Re-enable workspace resizing.\n if ((workspace as WorkspaceSvg).setResizesEnabled) {\n (workspace as WorkspaceSvg).setResizesEnabled(true);\n }\n eventUtils.fire(new (eventUtils.get(eventUtils.FINISHED_LOADING))(workspace));\n return newBlockIds;\n}\n\n/**\n * Decode an XML DOM and create blocks on the workspace. Position the new\n * blocks immediately below prior blocks, aligned by their starting edge.\n *\n * @param xml The XML DOM.\n * @param workspace The workspace to add to.\n * @returns An array containing new block IDs.\n * @alias Blockly.Xml.appendDomToWorkspace\n */\nexport function appendDomToWorkspace(\n xml: Element, workspace: WorkspaceSvg): string[] {\n // First check if we have a WorkspaceSvg, otherwise the blocks have no shape\n // and the position does not matter.\n // Assume it is rendered so we can check.\n if (!(workspace as WorkspaceSvg).getBlocksBoundingBox) {\n return domToWorkspace(xml, workspace);\n }\n\n const bbox = (workspace as WorkspaceSvg).getBlocksBoundingBox();\n // Load the new blocks into the workspace and get the IDs of the new blocks.\n const newBlockIds = domToWorkspace(xml, workspace);\n if (bbox && bbox.top !== bbox.bottom) { // Check if any previous block.\n let offsetY = 0; // Offset to add to y of the new block.\n let offsetX = 0;\n const farY = bbox.bottom; // Bottom position.\n const topX = workspace.RTL ? bbox.right : bbox.left; // X of bounding box.\n // Check position of the new blocks.\n let newLeftX = Infinity; // X of top left corner.\n let newRightX = -Infinity; // X of top right corner.\n let newY = Infinity; // Y of top corner.\n const ySeparation = 10;\n for (let i = 0; i < newBlockIds.length; i++) {\n const blockXY =\n workspace.getBlockById(newBlockIds[i])!.getRelativeToSurfaceXY();\n if (blockXY.y < newY) {\n newY = blockXY.y;\n }\n if (blockXY.x < newLeftX) { // if we left align also on x\n newLeftX = blockXY.x;\n }\n if (blockXY.x > newRightX) { // if we right align also on x\n newRightX = blockXY.x;\n }\n }\n offsetY = farY - newY + ySeparation;\n offsetX = workspace.RTL ? topX - newRightX : topX - newLeftX;\n for (let i = 0; i < newBlockIds.length; i++) {\n const block = workspace.getBlockById(newBlockIds[i]);\n block!.moveBy(offsetX, offsetY);\n }\n }\n return newBlockIds;\n}\n\n/**\n * Decode an XML block tag and create a block (and possibly sub blocks) on the\n * workspace.\n *\n * @param xmlBlock XML block element.\n * @param workspace The workspace.\n * @returns The root block created.\n * @alias Blockly.Xml.domToBlock\n */\nexport function domToBlock(xmlBlock: Element, workspace: Workspace): Block {\n // Create top-level block.\n eventUtils.disable();\n const variablesBeforeCreation = workspace.getAllVariables();\n let topBlock;\n try {\n topBlock = domToBlockHeadless(xmlBlock, workspace);\n // Generate list of all blocks.\n if (workspace.rendered) {\n const topBlockSvg = topBlock as BlockSvg;\n const blocks = topBlock.getDescendants(false);\n topBlockSvg.setConnectionTracking(false);\n // Render each block.\n for (let i = blocks.length - 1; i >= 0; i--) {\n (blocks[i] as BlockSvg).initSvg();\n }\n for (let i = blocks.length - 1; i >= 0; i--) {\n (blocks[i] as BlockSvg).render(false);\n }\n // Populating the connection database may be deferred until after the\n // blocks have rendered.\n setTimeout(function() {\n if (!topBlockSvg.disposed) {\n topBlockSvg.setConnectionTracking(true);\n }\n }, 1);\n topBlockSvg.updateDisabled();\n // Allow the scrollbars to resize and move based on the new contents.\n // TODO(@picklesrus): #387. Remove when domToBlock avoids resizing.\n (workspace as WorkspaceSvg).resizeContents();\n } else {\n const blocks = topBlock.getDescendants(false);\n for (let i = blocks.length - 1; i >= 0; i--) {\n blocks[i].initModel();\n }\n }\n } finally {\n eventUtils.enable();\n }\n if (eventUtils.isEnabled()) {\n // AnyDuringMigration because: Property 'get' does not exist on type\n // '(name: string) => void'.\n const newVariables =\n Variables.getAddedVariables(workspace, variablesBeforeCreation);\n // Fire a VarCreate event for each (if any) new variable created.\n for (let i = 0; i < newVariables.length; i++) {\n const thisVariable = newVariables[i];\n eventUtils.fire(\n new (eventUtils.get(eventUtils.VAR_CREATE))(thisVariable));\n }\n // Block events come after var events, in case they refer to newly created\n // variables.\n eventUtils.fire(new (eventUtils.get(eventUtils.CREATE))(topBlock));\n }\n return topBlock;\n}\n\n/**\n * Decode an XML list of variables and add the variables to the workspace.\n *\n * @param xmlVariables List of XML variable elements.\n * @param workspace The workspace to which the variable should be added.\n * @alias Blockly.Xml.domToVariables\n */\nexport function domToVariables(xmlVariables: Element, workspace: Workspace) {\n for (let i = 0; i < xmlVariables.children.length; i++) {\n const xmlChild = xmlVariables.children[i];\n const type = xmlChild.getAttribute('type');\n const id = xmlChild.getAttribute('id');\n const name = xmlChild.textContent;\n\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n workspace.createVariable(name as AnyDuringMigration, type, id);\n }\n}\n\n/** A mapping of nodeName to node for child nodes of xmlBlock. */\ninterface childNodeTagMap {\n mutation: Element[];\n comment: Element[];\n data: Element[];\n field: Element[];\n input: Element[];\n next: Element[];\n}\n\n/**\n * Creates a mapping of childNodes for each supported XML tag for the provided\n * xmlBlock. Logs a warning for any encountered unsupported tags.\n *\n * @param xmlBlock XML block element.\n * @returns The childNode map from nodeName to node.\n */\nfunction mapSupportedXmlTags(xmlBlock: Element): childNodeTagMap {\n const childNodeMap = {\n mutation: new Array(),\n comment: new Array(),\n data: new Array(),\n field: new Array(),\n input: new Array(),\n next: new Array(),\n };\n for (let i = 0; i < xmlBlock.children.length; i++) {\n const xmlChild = xmlBlock.children[i];\n if (xmlChild.nodeType === dom.NodeType.TEXT_NODE) {\n // Ignore any text at the level. It's all whitespace anyway.\n continue;\n }\n switch (xmlChild.nodeName.toLowerCase()) {\n case 'mutation':\n childNodeMap.mutation.push(xmlChild);\n break;\n case 'comment':\n childNodeMap.comment.push(xmlChild);\n break;\n case 'data':\n childNodeMap.data.push(xmlChild);\n break;\n case 'title':\n // Titles were renamed to field in December 2013.\n // Fall through.\n case 'field':\n childNodeMap.field.push(xmlChild);\n break;\n case 'value':\n case 'statement':\n childNodeMap.input.push(xmlChild);\n break;\n case 'next':\n childNodeMap.next.push(xmlChild);\n break;\n default:\n // Unknown tag; ignore. Same principle as HTML parsers.\n console.warn('Ignoring unknown tag: ' + xmlChild.nodeName);\n }\n }\n return childNodeMap;\n}\n\n/**\n * Applies mutation tag child nodes to the given block.\n *\n * @param xmlChildren Child nodes.\n * @param block The block to apply the child nodes on.\n * @returns True if mutation may have added some elements that need\n * initialization (requiring initSvg call).\n */\nfunction applyMutationTagNodes(xmlChildren: Element[], block: Block): boolean {\n let shouldCallInitSvg = false;\n for (let i = 0; i < xmlChildren.length; i++) {\n const xmlChild = xmlChildren[i];\n // Custom data for an advanced block.\n if (block.domToMutation) {\n block.domToMutation(xmlChild);\n if ((block as BlockSvg).initSvg) {\n // Mutation may have added some elements that need initializing.\n shouldCallInitSvg = true;\n }\n }\n }\n return shouldCallInitSvg;\n}\n\n/**\n * Applies comment tag child nodes to the given block.\n *\n * @param xmlChildren Child nodes.\n * @param block The block to apply the child nodes on.\n */\nfunction applyCommentTagNodes(xmlChildren: Element[], block: Block) {\n for (let i = 0; i < xmlChildren.length; i++) {\n const xmlChild = xmlChildren[i];\n const text = xmlChild.textContent;\n const pinned = xmlChild.getAttribute('pinned') === 'true';\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n const width = parseInt(xmlChild.getAttribute('w') as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n const height = parseInt(xmlChild.getAttribute('h') as AnyDuringMigration);\n\n block.setCommentText(text);\n block.commentModel.pinned = pinned;\n if (!isNaN(width) && !isNaN(height)) {\n block.commentModel.size = new Size(width, height);\n }\n\n if (pinned && (block as BlockSvg).getCommentIcon && !block.isInFlyout) {\n const blockSvg = block as BlockSvg;\n setTimeout(function() {\n blockSvg.getCommentIcon()!.setVisible(true);\n }, 1);\n }\n }\n}\n\n/**\n * Applies data tag child nodes to the given block.\n *\n * @param xmlChildren Child nodes.\n * @param block The block to apply the child nodes on.\n */\nfunction applyDataTagNodes(xmlChildren: Element[], block: Block) {\n for (let i = 0; i < xmlChildren.length; i++) {\n const xmlChild = xmlChildren[i];\n block.data = xmlChild.textContent;\n }\n}\n\n/**\n * Applies field tag child nodes to the given block.\n *\n * @param xmlChildren Child nodes.\n * @param block The block to apply the child nodes on.\n */\nfunction applyFieldTagNodes(xmlChildren: Element[], block: Block) {\n for (let i = 0; i < xmlChildren.length; i++) {\n const xmlChild = xmlChildren[i];\n const nodeName = xmlChild.getAttribute('name');\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n domToField(block, nodeName as AnyDuringMigration, xmlChild);\n }\n}\n\n/**\n * Finds any enclosed blocks or shadows within this XML node.\n *\n * @param xmlNode The XML node to extract child block info from.\n * @returns Any found child block.\n */\nfunction findChildBlocks(xmlNode: Element):\n {childBlockElement: Element|null, childShadowElement: Element|null} {\n const childBlockInfo = {childBlockElement: null, childShadowElement: null};\n for (let i = 0; i < xmlNode.childNodes.length; i++) {\n const xmlChild = xmlNode.childNodes[i];\n if (xmlChild.nodeType === dom.NodeType.ELEMENT_NODE) {\n if (xmlChild.nodeName.toLowerCase() === 'block') {\n // AnyDuringMigration because: Type 'Element' is not assignable to type\n // 'null'.\n childBlockInfo.childBlockElement =\n xmlChild as Element as AnyDuringMigration;\n } else if (xmlChild.nodeName.toLowerCase() === 'shadow') {\n // AnyDuringMigration because: Type 'Element' is not assignable to type\n // 'null'.\n childBlockInfo.childShadowElement =\n xmlChild as Element as AnyDuringMigration;\n }\n }\n }\n return childBlockInfo;\n}\n/**\n * Applies input child nodes (value or statement) to the given block.\n *\n * @param xmlChildren Child nodes.\n * @param workspace The workspace containing the given block.\n * @param block The block to apply the child nodes on.\n * @param prototypeName The prototype name of the block.\n */\nfunction applyInputTagNodes(\n xmlChildren: Element[], workspace: Workspace, block: Block,\n prototypeName: string) {\n for (let i = 0; i < xmlChildren.length; i++) {\n const xmlChild = xmlChildren[i];\n const nodeName = xmlChild.getAttribute('name');\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string'.\n const input = block.getInput(nodeName as AnyDuringMigration);\n if (!input) {\n console.warn(\n 'Ignoring non-existent input ' + nodeName + ' in block ' +\n prototypeName);\n break;\n }\n const childBlockInfo = findChildBlocks(xmlChild);\n if (childBlockInfo.childBlockElement) {\n if (!input.connection) {\n throw TypeError('Input connection does not exist.');\n }\n domToBlockHeadless(\n childBlockInfo.childBlockElement, workspace, input.connection, false);\n }\n // Set shadow after so we don't create a shadow we delete immediately.\n if (childBlockInfo.childShadowElement) {\n input.connection?.setShadowDom(childBlockInfo.childShadowElement);\n }\n }\n}\n\n/**\n * Applies next child nodes to the given block.\n *\n * @param xmlChildren Child nodes.\n * @param workspace The workspace containing the given block.\n * @param block The block to apply the child nodes on.\n */\nfunction applyNextTagNodes(\n xmlChildren: Element[], workspace: Workspace, block: Block) {\n for (let i = 0; i < xmlChildren.length; i++) {\n const xmlChild = xmlChildren[i];\n const childBlockInfo = findChildBlocks(xmlChild);\n if (childBlockInfo.childBlockElement) {\n if (!block.nextConnection) {\n throw TypeError('Next statement does not exist.');\n }\n // If there is more than one XML 'next' tag.\n if (block.nextConnection.isConnected()) {\n throw TypeError('Next statement is already connected.');\n }\n // Create child block.\n domToBlockHeadless(\n childBlockInfo.childBlockElement, workspace, block.nextConnection,\n true);\n }\n // Set shadow after so we don't create a shadow we delete immediately.\n if (childBlockInfo.childShadowElement && block.nextConnection) {\n block.nextConnection.setShadowDom(childBlockInfo.childShadowElement);\n }\n }\n}\n\n/**\n * Decode an XML block tag and create a block (and possibly sub blocks) on the\n * workspace.\n *\n * @param xmlBlock XML block element.\n * @param workspace The workspace.\n * @param parentConnection The parent connection to to connect this block to\n * after instantiating.\n * @param connectedToParentNext Whether the provided parent connection is a next\n * connection, rather than output or statement.\n * @returns The root block created.\n */\nfunction domToBlockHeadless(\n xmlBlock: Element, workspace: Workspace, parentConnection?: Connection,\n connectedToParentNext?: boolean): Block {\n let block = null;\n const prototypeName = xmlBlock.getAttribute('type');\n if (!prototypeName) {\n throw TypeError('Block type unspecified: ' + xmlBlock.outerHTML);\n }\n const id = xmlBlock.getAttribute('id');\n // AnyDuringMigration because: Argument of type 'string | null' is not\n // assignable to parameter of type 'string | undefined'.\n block = workspace.newBlock(prototypeName, id as AnyDuringMigration);\n\n // Preprocess childNodes so tags can be processed in a consistent order.\n const xmlChildNameMap = mapSupportedXmlTags(xmlBlock);\n\n const shouldCallInitSvg =\n applyMutationTagNodes(xmlChildNameMap.mutation, block);\n applyCommentTagNodes(xmlChildNameMap.comment, block);\n applyDataTagNodes(xmlChildNameMap.data, block);\n\n // Connect parent after processing mutation and before setting fields.\n if (parentConnection) {\n if (connectedToParentNext) {\n if (block.previousConnection) {\n parentConnection.connect(block.previousConnection);\n } else {\n throw TypeError('Next block does not have previous statement.');\n }\n } else {\n if (block.outputConnection) {\n parentConnection.connect(block.outputConnection);\n } else if (block.previousConnection) {\n parentConnection.connect(block.previousConnection);\n } else {\n throw TypeError(\n 'Child block does not have output or previous statement.');\n }\n }\n }\n\n applyFieldTagNodes(xmlChildNameMap.field, block);\n applyInputTagNodes(xmlChildNameMap.input, workspace, block, prototypeName);\n applyNextTagNodes(xmlChildNameMap.next, workspace, block);\n\n if (shouldCallInitSvg) {\n // This shouldn't even be called here\n // (ref: https://github.com/google/blockly/pull/4296#issuecomment-884226021\n // But the XML serializer/deserializer is iceboxed so I'm not going to fix\n // it.\n (block as BlockSvg).initSvg();\n }\n\n const inline = xmlBlock.getAttribute('inline');\n if (inline) {\n block.setInputsInline(inline === 'true');\n }\n const disabled = xmlBlock.getAttribute('disabled');\n if (disabled) {\n block.setEnabled(disabled !== 'true' && disabled !== 'disabled');\n }\n const deletable = xmlBlock.getAttribute('deletable');\n if (deletable) {\n block.setDeletable(deletable === 'true');\n }\n const movable = xmlBlock.getAttribute('movable');\n if (movable) {\n block.setMovable(movable === 'true');\n }\n const editable = xmlBlock.getAttribute('editable');\n if (editable) {\n block.setEditable(editable === 'true');\n }\n const collapsed = xmlBlock.getAttribute('collapsed');\n if (collapsed) {\n block.setCollapsed(collapsed === 'true');\n }\n if (xmlBlock.nodeName.toLowerCase() === 'shadow') {\n // Ensure all children are also shadows.\n const children = block.getChildren(false);\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (!child.isShadow()) {\n throw TypeError('Shadow block not allowed non-shadow child.');\n }\n }\n // Ensure this block doesn't have any variable inputs.\n if (block.getVarModels().length) {\n throw TypeError('Shadow blocks cannot have variable references.');\n }\n block.setShadow(true);\n }\n return block;\n}\n\n/**\n * Decode an XML field tag and set the value of that field on the given block.\n *\n * @param block The block that is currently being deserialized.\n * @param fieldName The name of the field on the block.\n * @param xml The field tag to decode.\n */\nfunction domToField(block: Block, fieldName: string, xml: Element) {\n const field = block.getField(fieldName);\n if (!field) {\n console.warn(\n 'Ignoring non-existent field ' + fieldName + ' in block ' + block.type);\n return;\n }\n field.fromXml(xml);\n}\n\n/**\n * Remove any 'next' block (statements in a stack).\n *\n * @param xmlBlock XML block element or an empty DocumentFragment if the block\n * was an insertion marker.\n * @alias Blockly.Xml.deleteNext\n */\nexport function deleteNext(xmlBlock: Element|DocumentFragment) {\n for (let i = 0; i < xmlBlock.childNodes.length; i++) {\n const child = xmlBlock.childNodes[i];\n if (child.nodeName.toLowerCase() === 'next') {\n xmlBlock.removeChild(child);\n break;\n }\n }\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods for string manipulation.\n * These methods are not specific to Blockly, and could be factored out into\n * a JavaScript framework such as Closure.\n *\n * @namespace Blockly.utils.string\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.string');\n\nimport * as deprecation from './deprecation.js';\n\n\n/**\n * Fast prefix-checker.\n * Copied from Closure's goog.string.startsWith.\n *\n * @param str The string to check.\n * @param prefix A string to look for at the start of `str`.\n * @returns True if `str` begins with `prefix`.\n * @alias Blockly.utils.string.startsWith\n * @deprecated Use built-in **string.startsWith** instead.\n */\nexport function startsWith(str: string, prefix: string): boolean {\n deprecation.warn(\n 'Blockly.utils.string.startsWith()', 'April 2022', 'April 2023',\n 'Use built-in string.startsWith');\n return str.startsWith(prefix);\n}\n\n/**\n * Given an array of strings, return the length of the shortest one.\n *\n * @param array Array of strings.\n * @returns Length of shortest string.\n * @alias Blockly.utils.string.shortestStringLength\n */\nexport function shortestStringLength(array: string[]): number {\n if (!array.length) {\n return 0;\n }\n return array\n .reduce(function(a, b) {\n return a.length < b.length ? a : b;\n })\n .length;\n}\n\n/**\n * Given an array of strings, return the length of the common prefix.\n * Words may not be split. Any space after a word is included in the length.\n *\n * @param array Array of strings.\n * @param opt_shortest Length of shortest string.\n * @returns Length of common prefix.\n * @alias Blockly.utils.string.commonWordPrefix\n */\nexport function commonWordPrefix(\n array: string[], opt_shortest?: number): number {\n if (!array.length) {\n return 0;\n } else if (array.length === 1) {\n return array[0].length;\n }\n let wordPrefix = 0;\n const max = opt_shortest || shortestStringLength(array);\n let len;\n for (len = 0; len < max; len++) {\n const letter = array[0][len];\n for (let i = 1; i < array.length; i++) {\n if (letter !== array[i][len]) {\n return wordPrefix;\n }\n }\n if (letter === ' ') {\n wordPrefix = len + 1;\n }\n }\n for (let i = 1; i < array.length; i++) {\n const letter = array[i][len];\n if (letter && letter !== ' ') {\n return wordPrefix;\n }\n }\n return max;\n}\n\n/**\n * Given an array of strings, return the length of the common suffix.\n * Words may not be split. Any space after a word is included in the length.\n *\n * @param array Array of strings.\n * @param opt_shortest Length of shortest string.\n * @returns Length of common suffix.\n * @alias Blockly.utils.string.commonWordSuffix\n */\nexport function commonWordSuffix(\n array: string[], opt_shortest?: number): number {\n if (!array.length) {\n return 0;\n } else if (array.length === 1) {\n return array[0].length;\n }\n let wordPrefix = 0;\n const max = opt_shortest || shortestStringLength(array);\n let len;\n for (len = 0; len < max; len++) {\n const letter = array[0].substr(-len - 1, 1);\n for (let i = 1; i < array.length; i++) {\n if (letter !== array[i].substr(-len - 1, 1)) {\n return wordPrefix;\n }\n }\n if (letter === ' ') {\n wordPrefix = len + 1;\n }\n }\n for (let i = 1; i < array.length; i++) {\n const letter = array[i].charAt(array[i].length - len - 1);\n if (letter && letter !== ' ') {\n return wordPrefix;\n }\n }\n return max;\n}\n\n/**\n * Wrap text to the specified width.\n *\n * @param text Text to wrap.\n * @param limit Width to wrap each line.\n * @returns Wrapped text.\n * @alias Blockly.utils.string.wrap\n */\nexport function wrap(text: string, limit: number): string {\n const lines = text.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n lines[i] = wrapLine(lines[i], limit);\n }\n return lines.join('\\n');\n}\n\n/**\n * Wrap single line of text to the specified width.\n *\n * @param text Text to wrap.\n * @param limit Width to wrap each line.\n * @returns Wrapped text.\n */\nfunction wrapLine(text: string, limit: number): string {\n if (text.length <= limit) {\n // Short text, no need to wrap.\n return text;\n }\n // Split the text into words.\n const words = text.trim().split(/\\s+/);\n // Set limit to be the length of the largest word.\n for (let i = 0; i < words.length; i++) {\n if (words[i].length > limit) {\n limit = words[i].length;\n }\n }\n\n let lastScore;\n let score = -Infinity;\n let lastText;\n let lineCount = 1;\n do {\n lastScore = score;\n lastText = text;\n // Create a list of booleans representing if a space (false) or\n // a break (true) appears after each word.\n let wordBreaks = [];\n // Seed the list with evenly spaced linebreaks.\n const steps = words.length / lineCount;\n let insertedBreaks = 1;\n for (let i = 0; i < words.length - 1; i++) {\n if (insertedBreaks < (i + 1.5) / steps) {\n insertedBreaks++;\n wordBreaks[i] = true;\n } else {\n wordBreaks[i] = false;\n }\n }\n wordBreaks = wrapMutate(words, wordBreaks, limit);\n score = wrapScore(words, wordBreaks, limit);\n text = wrapToText(words, wordBreaks);\n lineCount++;\n } while (score > lastScore);\n return lastText;\n}\n\n/**\n * Compute a score for how good the wrapping is.\n *\n * @param words Array of each word.\n * @param wordBreaks Array of line breaks.\n * @param limit Width to wrap each line.\n * @returns Larger the better.\n */\nfunction wrapScore(\n words: string[], wordBreaks: boolean[], limit: number): number {\n // If this function becomes a performance liability, add caching.\n // Compute the length of each line.\n const lineLengths = [0];\n const linePunctuation = [];\n for (let i = 0; i < words.length; i++) {\n lineLengths[lineLengths.length - 1] += words[i].length;\n if (wordBreaks[i] === true) {\n lineLengths.push(0);\n linePunctuation.push(words[i].charAt(words[i].length - 1));\n } else if (wordBreaks[i] === false) {\n lineLengths[lineLengths.length - 1]++;\n }\n }\n const maxLength = Math.max(...lineLengths);\n\n let score = 0;\n for (let i = 0; i < lineLengths.length; i++) {\n // Optimize for width.\n // -2 points per char over limit (scaled to the power of 1.5).\n score -= Math.pow(Math.abs(limit - lineLengths[i]), 1.5) * 2;\n // Optimize for even lines.\n // -1 point per char smaller than max (scaled to the power of 1.5).\n score -= Math.pow(maxLength - lineLengths[i], 1.5);\n // Optimize for structure.\n // Add score to line endings after punctuation.\n if ('.?!'.indexOf(linePunctuation[i]) !== -1) {\n score += limit / 3;\n } else if (',;)]}'.indexOf(linePunctuation[i]) !== -1) {\n score += limit / 4;\n }\n }\n // All else being equal, the last line should not be longer than the\n // previous line. For example, this looks wrong:\n // aaa bbb\n // ccc ddd eee\n if (lineLengths.length > 1 &&\n lineLengths[lineLengths.length - 1] <=\n lineLengths[lineLengths.length - 2]) {\n score += 0.5;\n }\n return score;\n}\n/**\n * Mutate the array of line break locations until an optimal solution is found.\n * No line breaks are added or deleted, they are simply moved around.\n *\n * @param words Array of each word.\n * @param wordBreaks Array of line breaks.\n * @param limit Width to wrap each line.\n * @returns New array of optimal line breaks.\n */\nfunction wrapMutate(\n words: string[], wordBreaks: boolean[], limit: number): boolean[] {\n let bestScore = wrapScore(words, wordBreaks, limit);\n let bestBreaks;\n // Try shifting every line break forward or backward.\n for (let i = 0; i < wordBreaks.length - 1; i++) {\n if (wordBreaks[i] === wordBreaks[i + 1]) {\n continue;\n }\n const mutatedWordBreaks = (new Array()).concat(wordBreaks);\n mutatedWordBreaks[i] = !mutatedWordBreaks[i];\n mutatedWordBreaks[i + 1] = !mutatedWordBreaks[i + 1];\n const mutatedScore = wrapScore(words, mutatedWordBreaks, limit);\n if (mutatedScore > bestScore) {\n bestScore = mutatedScore;\n bestBreaks = mutatedWordBreaks;\n }\n }\n if (bestBreaks) {\n // Found an improvement. See if it may be improved further.\n return wrapMutate(words, bestBreaks, limit);\n }\n // No improvements found. Done.\n return wordBreaks;\n}\n\n/**\n * Reassemble the array of words into text, with the specified line breaks.\n *\n * @param words Array of each word.\n * @param wordBreaks Array of line breaks.\n * @returns Plain text.\n */\nfunction wrapToText(words: string[], wordBreaks: boolean[]): string {\n const text = [];\n for (let i = 0; i < words.length; i++) {\n text.push(words[i]);\n if (wordBreaks[i] !== undefined) {\n text.push(wordBreaks[i] ? '\\n' : ' ');\n }\n }\n return text.join('');\n}\n\n/**\n * Is the given string a number (includes negative and decimals).\n *\n * @param str Input string.\n * @returns True if number, false otherwise.\n * @alias Blockly.utils.string.isNumber\n */\nexport function isNumber(str: string): boolean {\n return /^\\s*-?\\d+(\\.\\d+)?\\s*$/.test(str);\n}\n","/**\n * @license\n * Copyright 2011 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Library to create tooltips for Blockly.\n * First, call createDom() after onload.\n * Second, set the 'tooltip' property on any SVG element that needs a tooltip.\n * If the tooltip is a string, or a function that returns a string, that message\n * will be displayed. If the tooltip is an SVG element, then that object's\n * tooltip will be used. Third, call bindMouseEvents(e) passing the SVG element.\n *\n * @namespace Blockly.Tooltip\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Tooltip');\n\nimport * as browserEvents from './browser_events.js';\nimport * as common from './common.js';\nimport * as blocklyString from './utils/string.js';\n\n\n/**\n * A type which can define a tooltip.\n * Either a string, an object containing a tooltip property, or a function which\n * returns either a string, or another arbitrarily nested function which\n * eventually unwinds to a string.\n *\n * @alias Blockly.Tooltip.TipInfo\n */\nexport type TipInfo =\n string|{tooltip: AnyDuringMigration}|(() => TipInfo|string|Function);\n\n/**\n * A function that renders custom tooltip UI.\n * 1st parameter: the div element to render content into.\n * 2nd parameter: the element being moused over (i.e., the element for which the\n * tooltip should be shown).\n *\n * @alias Blockly.Tooltip.CustomTooltip\n */\nexport type CustomTooltip = (p1: Element, p2: Element) => AnyDuringMigration;\n\n/**\n * An optional function that renders custom tooltips into the provided DIV. If\n * this is defined, the function will be called instead of rendering the default\n * tooltip UI.\n */\nlet customTooltip: CustomTooltip|undefined = undefined;\n\n/**\n * Sets a custom function that will be called if present instead of the default\n * tooltip UI.\n *\n * @param customFn A custom tooltip used to render an alternate tooltip UI.\n * @alias Blockly.Tooltip.setCustomTooltip\n */\nexport function setCustomTooltip(customFn: CustomTooltip) {\n customTooltip = customFn;\n}\n\n/**\n * Gets the custom tooltip function.\n *\n * @returns The custom tooltip function, if defined.\n */\nexport function getCustomTooltip(): CustomTooltip|undefined {\n return customTooltip;\n}\n\n/** Is a tooltip currently showing? */\nlet visible = false;\n\n/**\n * Returns whether or not a tooltip is showing\n *\n * @returns True if a tooltip is showing\n * @alias Blockly.Tooltip.isVisible\n */\nexport function isVisible(): boolean {\n return visible;\n}\n\n/** Is someone else blocking the tooltip from being shown? */\nlet blocked = false;\n\n/**\n * Maximum width (in characters) of a tooltip.\n *\n * @alias Blockly.Tooltip.LIMIT\n */\nexport const LIMIT = 50;\n\n/** PID of suspended thread to clear tooltip on mouse out. */\nlet mouseOutPid: AnyDuringMigration = 0;\n\n/** PID of suspended thread to show the tooltip. */\nlet showPid: AnyDuringMigration = 0;\n\n/**\n * Last observed X location of the mouse pointer (freezes when tooltip appears).\n */\nlet lastX = 0;\n\n/**\n * Last observed Y location of the mouse pointer (freezes when tooltip appears).\n */\nlet lastY = 0;\n\n/** Current element being pointed at. */\nlet element: AnyDuringMigration = null;\n\n/**\n * Once a tooltip has opened for an element, that element is 'poisoned' and\n * cannot respawn a tooltip until the pointer moves over a different element.\n */\nlet poisonedElement: AnyDuringMigration = null;\n\n/**\n * Horizontal offset between mouse cursor and tooltip.\n *\n * @alias Blockly.Tooltip.OFFSET_X\n */\nexport const OFFSET_X = 0;\n\n/**\n * Vertical offset between mouse cursor and tooltip.\n *\n * @alias Blockly.Tooltip.OFFSET_Y\n */\nexport const OFFSET_Y = 10;\n\n/**\n * Radius mouse can move before killing tooltip.\n *\n * @alias Blockly.Tooltip.RADIUS_OK\n */\nexport const RADIUS_OK = 10;\n\n/**\n * Delay before tooltip appears.\n *\n * @alias Blockly.Tooltip.HOVER_MS\n */\nexport const HOVER_MS = 750;\n\n/**\n * Horizontal padding between tooltip and screen edge.\n *\n * @alias Blockly.Tooltip.MARGINS\n */\nexport const MARGINS = 5;\n\n/** The HTML container. Set once by createDom. */\nlet containerDiv: HTMLDivElement|null = null;\n\n/**\n * Returns the HTML tooltip container.\n *\n * @returns The HTML tooltip container.\n * @alias Blockly.Tooltip.getDiv\n */\nexport function getDiv(): HTMLDivElement|null {\n return containerDiv;\n}\n\n/**\n * Returns the tooltip text for the given element.\n *\n * @param object The object to get the tooltip text of.\n * @returns The tooltip text of the element.\n * @alias Blockly.Tooltip.getTooltipOfObject\n */\nexport function getTooltipOfObject(object: AnyDuringMigration|null): string {\n const obj = getTargetObject(object);\n if (obj) {\n let tooltip = obj.tooltip;\n while (typeof tooltip === 'function') {\n tooltip = tooltip();\n }\n if (typeof tooltip !== 'string') {\n throw Error('Tooltip function must return a string.');\n }\n return tooltip;\n }\n return '';\n}\n\n/**\n * Returns the target object that the given object is targeting for its\n * tooltip. Could be the object itself.\n *\n * @param obj The object are trying to find the target tooltip object of.\n * @returns The target tooltip object.\n */\nfunction getTargetObject(obj: object|null): {tooltip: AnyDuringMigration}|null {\n while (obj && (obj as any).tooltip) {\n if (typeof (obj as any).tooltip === 'string' ||\n typeof (obj as any).tooltip === 'function') {\n return obj as {tooltip: string | (() => string)};\n }\n obj = (obj as any).tooltip;\n }\n return null;\n}\n\n/**\n * Create the tooltip div and inject it onto the page.\n *\n * @alias Blockly.Tooltip.createDom\n */\nexport function createDom() {\n if (containerDiv) {\n return; // Already created.\n }\n // Create an HTML container for popup overlays (e.g. editor widgets).\n containerDiv = document.createElement('div');\n containerDiv.className = 'blocklyTooltipDiv';\n const container = common.getParentContainer() || document.body;\n container.appendChild(containerDiv);\n}\n\n/**\n * Binds the required mouse events onto an SVG element.\n *\n * @param element SVG element onto which tooltip is to be bound.\n * @alias Blockly.Tooltip.bindMouseEvents\n */\nexport function bindMouseEvents(element: Element) {\n // TODO (#6097): Don't stash wrapper info on the DOM.\n (element as AnyDuringMigration).mouseOverWrapper_ =\n browserEvents.bind(element, 'mouseover', null, onMouseOver);\n (element as AnyDuringMigration).mouseOutWrapper_ =\n browserEvents.bind(element, 'mouseout', null, onMouseOut);\n\n // Don't use bindEvent_ for mousemove since that would create a\n // corresponding touch handler, even though this only makes sense in the\n // context of a mouseover/mouseout.\n element.addEventListener('mousemove', onMouseMove, false);\n}\n\n/**\n * Unbinds tooltip mouse events from the SVG element.\n *\n * @param element SVG element onto which tooltip is bound.\n * @alias Blockly.Tooltip.unbindMouseEvents\n */\nexport function unbindMouseEvents(element: Element|null) {\n if (!element) {\n return;\n }\n // TODO (#6097): Don't stash wrapper info on the DOM.\n browserEvents.unbind((element as AnyDuringMigration).mouseOverWrapper_);\n browserEvents.unbind((element as AnyDuringMigration).mouseOutWrapper_);\n element.removeEventListener('mousemove', onMouseMove);\n}\n\n/**\n * Hide the tooltip if the mouse is over a different object.\n * Initialize the tooltip to potentially appear for this object.\n *\n * @param e Mouse event.\n */\nfunction onMouseOver(e: Event) {\n if (blocked) {\n // Someone doesn't want us to show tooltips.\n return;\n }\n // If the tooltip is an object, treat it as a pointer to the next object in\n // the chain to look at. Terminate when a string or function is found.\n const newElement = getTargetObject(e.currentTarget);\n if (element !== newElement) {\n hide();\n poisonedElement = null;\n element = newElement;\n }\n // Forget about any immediately preceding mouseOut event.\n clearTimeout(mouseOutPid);\n}\n\n/**\n * Hide the tooltip if the mouse leaves the object and enters the workspace.\n *\n * @param _e Mouse event.\n */\nfunction onMouseOut(_e: Event) {\n if (blocked) {\n // Someone doesn't want us to show tooltips.\n return;\n }\n // Moving from one element to another (overlapping or with no gap) generates\n // a mouseOut followed instantly by a mouseOver. Fork off the mouseOut\n // event and kill it if a mouseOver is received immediately.\n // This way the task only fully executes if mousing into the void.\n mouseOutPid = setTimeout(function() {\n element = null;\n poisonedElement = null;\n hide();\n }, 1);\n clearTimeout(showPid);\n}\n\n/**\n * When hovering over an element, schedule a tooltip to be shown. If a tooltip\n * is already visible, hide it if the mouse strays out of a certain radius.\n *\n * @param e Mouse event.\n */\nfunction onMouseMove(e: Event) {\n if (!element || !(element as AnyDuringMigration).tooltip) {\n // No tooltip here to show.\n return;\n } else if (blocked) {\n // Someone doesn't want us to show tooltips. We are probably handling a\n // user gesture, such as a click or drag.\n return;\n }\n if (visible) {\n // Compute the distance between the mouse position when the tooltip was\n // shown and the current mouse position. Pythagorean theorem.\n // AnyDuringMigration because: Property 'pageX' does not exist on type\n // 'Event'.\n const dx = lastX - (e as AnyDuringMigration).pageX;\n // AnyDuringMigration because: Property 'pageY' does not exist on type\n // 'Event'.\n const dy = lastY - (e as AnyDuringMigration).pageY;\n if (Math.sqrt(dx * dx + dy * dy) > RADIUS_OK) {\n hide();\n }\n } else if (poisonedElement !== element) {\n // The mouse moved, clear any previously scheduled tooltip.\n clearTimeout(showPid);\n // Maybe this time the mouse will stay put. Schedule showing of tooltip.\n // AnyDuringMigration because: Property 'pageX' does not exist on type\n // 'Event'.\n lastX = (e as AnyDuringMigration).pageX;\n // AnyDuringMigration because: Property 'pageY' does not exist on type\n // 'Event'.\n lastY = (e as AnyDuringMigration).pageY;\n showPid = setTimeout(show, HOVER_MS);\n }\n}\n\n/**\n * Dispose of the tooltip.\n *\n * @alias Blockly.Tooltip.dispose\n * @internal\n */\nexport function dispose() {\n element = null;\n poisonedElement = null;\n hide();\n}\n\n/**\n * Hide the tooltip.\n *\n * @alias Blockly.Tooltip.hide\n */\nexport function hide() {\n if (visible) {\n visible = false;\n if (containerDiv) {\n containerDiv.style.display = 'none';\n }\n }\n if (showPid) {\n clearTimeout(showPid);\n }\n}\n\n/**\n * Hide any in-progress tooltips and block showing new tooltips until the next\n * call to unblock().\n *\n * @alias Blockly.Tooltip.block\n * @internal\n */\nexport function block() {\n hide();\n blocked = true;\n}\n\n/**\n * Unblock tooltips: allow them to be scheduled and shown according to their own\n * logic.\n *\n * @alias Blockly.Tooltip.unblock\n * @internal\n */\nexport function unblock() {\n blocked = false;\n}\n\n/** Renders the tooltip content into the tooltip div. */\nfunction renderContent() {\n if (!containerDiv || !element) {\n // This shouldn't happen, but if it does, we can't render.\n return;\n }\n if (typeof customTooltip === 'function') {\n customTooltip(containerDiv, element);\n } else {\n renderDefaultContent();\n }\n}\n\n/** Renders the default tooltip UI. */\nfunction renderDefaultContent() {\n let tip = getTooltipOfObject(element);\n tip = blocklyString.wrap(tip, LIMIT);\n // Create new text, line by line.\n const lines = tip.split('\\n');\n for (let i = 0; i < lines.length; i++) {\n const div = (document.createElement('div'));\n div.appendChild(document.createTextNode(lines[i]));\n containerDiv!.appendChild(div);\n }\n}\n\n/**\n * Gets the coordinates for the tooltip div, taking into account the edges of\n * the screen to prevent showing the tooltip offscreen.\n *\n * @param rtl True if the tooltip should be in right-to-left layout.\n * @returns Coordinates at which the tooltip div should be placed.\n */\nfunction getPosition(rtl: boolean): {x: number, y: number} {\n // Position the tooltip just below the cursor.\n const windowWidth = document.documentElement.clientWidth;\n const windowHeight = document.documentElement.clientHeight;\n\n let anchorX = lastX;\n if (rtl) {\n anchorX -= OFFSET_X + containerDiv!.offsetWidth;\n } else {\n anchorX += OFFSET_X;\n }\n\n let anchorY = lastY + OFFSET_Y;\n if (anchorY + containerDiv!.offsetHeight > windowHeight + window.scrollY) {\n // Falling off the bottom of the screen; shift the tooltip up.\n anchorY -= containerDiv!.offsetHeight + 2 * OFFSET_Y;\n }\n\n if (rtl) {\n // Prevent falling off left edge in RTL mode.\n anchorX = Math.max(MARGINS - window.scrollX, anchorX);\n } else {\n if (anchorX + containerDiv!.offsetWidth >\n windowWidth + window.scrollX - 2 * MARGINS) {\n // Falling off the right edge of the screen;\n // clamp the tooltip on the edge.\n anchorX = windowWidth - containerDiv!.offsetWidth - 2 * MARGINS;\n }\n }\n\n return {x: anchorX, y: anchorY};\n}\n\n/** Create the tooltip and show it. */\nfunction show() {\n if (blocked) {\n // Someone doesn't want us to show tooltips.\n return;\n }\n poisonedElement = element;\n if (!containerDiv) {\n return;\n }\n // Erase all existing text.\n containerDiv.textContent = '';\n\n // Add new content.\n renderContent();\n\n // Display the tooltip.\n const rtl = (element as any).RTL;\n containerDiv.style.direction = rtl ? 'rtl' : 'ltr';\n containerDiv.style.display = 'block';\n visible = true;\n\n const {x, y} = getPosition(rtl);\n containerDiv.style.left = x + 'px';\n containerDiv.style.top = y + 'px';\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods for colour manipulation.\n *\n * @namespace Blockly.utils.colour\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.colour');\n\n\n/**\n * The richness of block colours, regardless of the hue.\n * Must be in the range of 0 (inclusive) to 1 (exclusive).\n *\n * @alias Blockly.utils.colour.hsvSaturation\n */\nlet hsvSaturation = 0.45;\n\n/**\n * Get the richness of block colours, regardless of the hue.\n *\n * @alias Blockly.utils.colour.getHsvSaturation\n * @returns The current richness.\n * @internal\n */\nexport function getHsvSaturation(): number {\n return hsvSaturation;\n}\n\n/**\n * Set the richness of block colours, regardless of the hue.\n *\n * @param newSaturation The new richness, in the range of 0 (inclusive) to 1\n * (exclusive)\n * @alias Blockly.utils.colour.setHsvSaturation\n * @internal\n */\nexport function setHsvSaturation(newSaturation: number) {\n hsvSaturation = newSaturation;\n}\n\n/**\n * The intensity of block colours, regardless of the hue.\n * Must be in the range of 0 (inclusive) to 1 (exclusive).\n *\n * @alias Blockly.utils.colour.hsvValue\n */\nlet hsvValue = 0.65;\n\n/**\n * Get the intensity of block colours, regardless of the hue.\n *\n * @alias Blockly.utils.colour.getHsvValue\n * @returns The current intensity.\n * @internal\n */\nexport function getHsvValue(): number {\n return hsvValue;\n}\n\n/**\n * Set the intensity of block colours, regardless of the hue.\n *\n * @param newValue The new intensity, in the range of 0 (inclusive) to 1\n * (exclusive)\n * @alias Blockly.utils.colour.setHsvValue\n * @internal\n */\nexport function setHsvValue(newValue: number) {\n hsvValue = newValue;\n}\n\n/**\n * Parses a colour from a string.\n * .parse('red') = '#ff0000'\n * .parse('#f00') = '#ff0000'\n * .parse('#ff0000') = '#ff0000'\n * .parse('0xff0000') = '#ff0000'\n * .parse('rgb(255, 0, 0)') = '#ff0000'\n *\n * @param str Colour in some CSS format.\n * @returns A string containing a hex representation of the colour, or null if\n * can't be parsed.\n * @alias Blockly.utils.colour.parse\n */\nexport function parse(str: string|number): string|null {\n str = String(str).toLowerCase().trim();\n let hex = names[str];\n if (hex) {\n // e.g. 'red'\n return hex;\n }\n hex = str.substring(0, 2) === '0x' ? '#' + str.substring(2) : str;\n hex = hex[0] === '#' ? hex : '#' + hex;\n if (/^#[0-9a-f]{6}$/.test(hex)) {\n // e.g. '#00ff88'\n return hex;\n }\n if (/^#[0-9a-f]{3}$/.test(hex)) {\n // e.g. '#0f8'\n return ['#', hex[1], hex[1], hex[2], hex[2], hex[3], hex[3]].join('');\n }\n const rgb = str.match(/^(?:rgb)?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)$/);\n if (rgb) {\n // e.g. 'rgb(0, 128, 255)'\n const r = Number(rgb[1]);\n const g = Number(rgb[2]);\n const b = Number(rgb[3]);\n if (r >= 0 && r < 256 && g >= 0 && g < 256 && b >= 0 && b < 256) {\n return rgbToHex(r, g, b);\n }\n }\n return null;\n}\n\n/**\n * Converts a colour from RGB to hex representation.\n *\n * @param r Amount of red, int between 0 and 255.\n * @param g Amount of green, int between 0 and 255.\n * @param b Amount of blue, int between 0 and 255.\n * @returns Hex representation of the colour.\n * @alias Blockly.utils.colour.rgbToHex\n */\nexport function rgbToHex(r: number, g: number, b: number): string {\n const rgb = r << 16 | g << 8 | b;\n if (r < 0x10) {\n return '#' + (0x1000000 | rgb).toString(16).substr(1);\n }\n return '#' + rgb.toString(16);\n}\n\n/**\n * Converts a colour to RGB.\n *\n * @param colour String representing colour in any colour format ('#ff0000',\n * 'red', '0xff000', etc).\n * @returns RGB representation of the colour.\n * @alias Blockly.utils.colour.hexToRgb\n */\nexport function hexToRgb(colour: string): number[] {\n const hex = parse(colour);\n if (!hex) {\n return [0, 0, 0];\n }\n\n const rgb = parseInt(hex.substr(1), 16);\n const r = rgb >> 16;\n const g = rgb >> 8 & 255;\n const b = rgb & 255;\n\n return [r, g, b];\n}\n\n/**\n * Converts an HSV triplet to hex representation.\n *\n * @param h Hue value in [0, 360].\n * @param s Saturation value in [0, 1].\n * @param v Brightness in [0, 255].\n * @returns Hex representation of the colour.\n * @alias Blockly.utils.colour.hsvToHex\n */\nexport function hsvToHex(h: number, s: number, v: number): string {\n let red = 0;\n let green = 0;\n let blue = 0;\n if (s === 0) {\n red = v;\n green = v;\n blue = v;\n } else {\n const sextant = Math.floor(h / 60);\n const remainder = h / 60 - sextant;\n const val1 = v * (1 - s);\n const val2 = v * (1 - s * remainder);\n const val3 = v * (1 - s * (1 - remainder));\n switch (sextant) {\n case 1:\n red = val2;\n green = v;\n blue = val1;\n break;\n case 2:\n red = val1;\n green = v;\n blue = val3;\n break;\n case 3:\n red = val1;\n green = val2;\n blue = v;\n break;\n case 4:\n red = val3;\n green = val1;\n blue = v;\n break;\n case 5:\n red = v;\n green = val1;\n blue = val2;\n break;\n case 6:\n case 0:\n red = v;\n green = val3;\n blue = val1;\n break;\n }\n }\n return rgbToHex(Math.floor(red), Math.floor(green), Math.floor(blue));\n}\n\n/**\n * Blend two colours together, using the specified factor to indicate the\n * weight given to the first colour.\n *\n * @param colour1 First colour.\n * @param colour2 Second colour.\n * @param factor The weight to be given to colour1 over colour2.\n * Values should be in the range [0, 1].\n * @returns Combined colour represented in hex.\n * @alias Blockly.utils.colour.blend\n */\nexport function blend(colour1: string, colour2: string, factor: number): string|\n null {\n const hex1 = parse(colour1);\n if (!hex1) {\n return null;\n }\n const hex2 = parse(colour2);\n if (!hex2) {\n return null;\n }\n const rgb1 = hexToRgb(hex1);\n const rgb2 = hexToRgb(hex2);\n const r = Math.round(rgb2[0] + factor * (rgb1[0] - rgb2[0]));\n const g = Math.round(rgb2[1] + factor * (rgb1[1] - rgb2[1]));\n const b = Math.round(rgb2[2] + factor * (rgb1[2] - rgb2[2]));\n return rgbToHex(r, g, b);\n}\n\n/**\n * A map that contains the 16 basic colour keywords as defined by W3C:\n * https://www.w3.org/TR/2018/REC-css-color-3-20180619/#html4\n * The keys of this map are the lowercase \"readable\" names of the colours,\n * while the values are the \"hex\" values.\n *\n * @alias Blockly.utils.colour.names\n */\nexport const names: {[key: string]: string} = {\n 'aqua': '#00ffff',\n 'black': '#000000',\n 'blue': '#0000ff',\n 'fuchsia': '#ff00ff',\n 'gray': '#808080',\n 'green': '#008000',\n 'lime': '#00ff00',\n 'maroon': '#800000',\n 'navy': '#000080',\n 'olive': '#808000',\n 'purple': '#800080',\n 'red': '#ff0000',\n 'silver': '#c0c0c0',\n 'teal': '#008080',\n 'white': '#ffffff',\n 'yellow': '#ffff00',\n};\n\n/**\n * Convert a hue (HSV model) into an RGB hex triplet.\n *\n * @param hue Hue on a colour wheel (0-360).\n * @returns RGB code, e.g. '#5ba65b'.\n * @alias Blockly.utils.colour.hueToHex\n */\nexport function hueToHex(hue: number): string {\n return hsvToHex(hue, hsvSaturation, hsvValue * 255);\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * @namespace Blockly.utils.parsing\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.parsing');\n\nimport {Msg} from '../msg.js';\n\nimport * as colourUtils from './colour.js';\n\n\n/**\n * Internal implementation of the message reference and interpolation token\n * parsing used by tokenizeInterpolation() and replaceMessageReferences().\n *\n * @param message Text which might contain string table references and\n * interpolation tokens.\n * @param parseInterpolationTokens Option to parse numeric\n * interpolation tokens (%1, %2, ...) when true.\n * @returns Array of strings and numbers.\n */\nfunction tokenizeInterpolationInternal(\n message: string, parseInterpolationTokens: boolean): (string|number)[] {\n const tokens = [];\n const chars = message.split('');\n chars.push( // End marker.\n '');\n // Parse the message with a finite state machine.\n // 0 - Base case.\n // 1 - % found.\n // 2 - Digit found.\n // 3 - Message ref found.\n let state = 0;\n const buffer = new Array();\n let number = null;\n for (let i = 0; i < chars.length; i++) {\n const c = chars[i];\n if (state === 0) { // Start escape.\n if (c === '%') {\n const text = buffer.join('');\n if (text) {\n tokens.push(text);\n }\n buffer.length = 0;\n state = 1;\n } else {\n buffer.push(c); // Regular char.\n }\n } else if (state === 1) {\n if (c === '%') {\n buffer.push(c); // Escaped %: %%\n state = 0;\n } else if (parseInterpolationTokens && '0' <= c && c <= '9') {\n state = 2;\n number = c;\n const text = buffer.join('');\n if (text) {\n tokens.push(text);\n }\n buffer.length = 0;\n } else if (c === '{') {\n state = 3;\n } else {\n buffer.push('%', c); // Not recognized. Return as literal.\n state = 0;\n }\n } else if (state === 2) {\n if ('0' <= c && c <= '9') {\n number += c; // Multi-digit number.\n } else {\n tokens.push(parseInt(number ?? '', 10));\n i--; // Parse this char again.\n state = 0;\n }\n } else if (state === 3) { // String table reference\n if (c === '') {\n // Premature end before closing '}'\n buffer.splice(0, 0, '%{'); // Re-insert leading delimiter\n i--; // Parse this char again.\n state = 0; // and parse as string literal.\n } else if (c !== '}') {\n buffer.push(c);\n } else {\n const rawKey = buffer.join('');\n if (/[A-Z]\\w*/i.test(rawKey)) { // Strict matching\n // Found a valid string key. Attempt case insensitive match.\n const keyUpper = rawKey.toUpperCase();\n\n // BKY_ is the prefix used to namespace the strings used in\n // Blockly core files and the predefined blocks in ../blocks/.\n // These strings are defined in ../msgs/ files.\n const bklyKey =\n keyUpper.startsWith('BKY_') ? keyUpper.substring(4) : null;\n if (bklyKey && bklyKey in Msg) {\n const rawValue = Msg[bklyKey];\n if (typeof rawValue === 'string') {\n // Attempt to dereference substrings, too, appending to the\n // end.\n Array.prototype.push.apply(\n tokens,\n tokenizeInterpolationInternal(\n rawValue, parseInterpolationTokens));\n } else if (parseInterpolationTokens) {\n // When parsing interpolation tokens, numbers are special\n // placeholders (%1, %2, etc). Make sure all other values are\n // strings.\n tokens.push(String(rawValue));\n } else {\n tokens.push(rawValue);\n }\n } else {\n // No entry found in the string table. Pass reference as string.\n tokens.push('%{' + rawKey + '}');\n }\n buffer.length = 0; // Clear the array\n state = 0;\n } else {\n tokens.push('%{' + rawKey + '}');\n buffer.length = 0;\n state = 0; // and parse as string literal.\n }\n }\n }\n }\n let text = buffer.join('');\n if (text) {\n tokens.push(text);\n }\n\n // Merge adjacent text tokens into a single string.\n const mergedTokens = [];\n buffer.length = 0;\n for (let i = 0; i < tokens.length; i++) {\n if (typeof tokens[i] === 'string') {\n buffer.push(tokens[i] as string);\n } else {\n text = buffer.join('');\n if (text) {\n mergedTokens.push(text);\n }\n buffer.length = 0;\n mergedTokens.push(tokens[i]);\n }\n }\n text = buffer.join('');\n if (text) {\n mergedTokens.push(text);\n }\n buffer.length = 0;\n\n return mergedTokens;\n}\n\n/**\n * Parse a string with any number of interpolation tokens (%1, %2, ...).\n * It will also replace string table references (e.g., %{bky_my_msg} and\n * %{BKY_MY_MSG} will both be replaced with the value in\n * Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped\n * (e.g., '%%').\n *\n * @param message Text which might contain string table references and\n * interpolation tokens.\n * @returns Array of strings and numbers.\n * @alias Blockly.utils.parsing.tokenizeInterpolation\n */\nexport function tokenizeInterpolation(message: string): (string|number)[] {\n return tokenizeInterpolationInternal(message, true);\n}\n\n/**\n * Replaces string table references in a message, if the message is a string.\n * For example, \"%{bky_my_msg}\" and \"%{BKY_MY_MSG}\" will both be replaced with\n * the value in Msg['MY_MSG'].\n *\n * @param message Message, which may be a string that contains\n * string table references.\n * @returns String with message references replaced.\n * @alias Blockly.utils.parsing.replaceMessageReferences\n */\nexport function replaceMessageReferences(message: string|any): string {\n if (typeof message !== 'string') {\n return message;\n }\n const interpolatedResult = tokenizeInterpolationInternal(message, false);\n // When parseInterpolationTokens === false, interpolatedResult should be at\n // most length 1.\n return interpolatedResult.length ? String(interpolatedResult[0]) : '';\n}\n\n/**\n * Validates that any %{MSG_KEY} references in the message refer to keys of\n * the Msg string table.\n *\n * @param message Text which might contain string table references.\n * @returns True if all message references have matching values.\n * Otherwise, false.\n * @alias Blockly.utils.parsing.checkMessageReferences\n */\nexport function checkMessageReferences(message: string): boolean {\n let validSoFar = true;\n\n const msgTable = Msg;\n // TODO (#1169): Implement support for other string tables,\n // prefixes other than BKY_.\n const m = message.match(/%{BKY_[A-Z]\\w*}/ig);\n if (m) {\n for (let i = 0; i < m.length; i++) {\n const msgKey = m[i].toUpperCase();\n if (msgTable[msgKey.slice(6, -1)] === undefined) {\n console.warn('No message string for ' + m[i] + ' in ' + message);\n validSoFar = false; // Continue to report other errors.\n }\n }\n }\n\n return validSoFar;\n}\n\n/**\n * Parse a block colour from a number or string, as provided in a block\n * definition.\n *\n * @param colour HSV hue value (0 to 360), #RRGGBB string,\n * or a message reference string pointing to one of those two values.\n * @returns An object containing the colour as\n * a #RRGGBB string, and the hue if the input was an HSV hue value.\n * @throws {Error} If the colour cannot be parsed.\n * @alias Blockly.utils.parsing.parseBlockColour\n */\nexport function parseBlockColour(colour: number|\n string): {hue: number|null, hex: string} {\n const dereferenced =\n typeof colour === 'string' ? replaceMessageReferences(colour) : colour;\n\n const hue = Number(dereferenced);\n if (!isNaN(hue) && 0 <= hue && hue <= 360) {\n return {\n hue: hue,\n hex: colourUtils.hsvToHex(\n hue, colourUtils.getHsvSaturation(), colourUtils.getHsvValue() * 255),\n };\n } else {\n const hex = colourUtils.parse(dereferenced);\n if (hex) {\n // Only store hue if colour is set as a hue.\n return {hue: null, hex: hex};\n } else {\n let errorMsg = 'Invalid colour: \"' + dereferenced + '\"';\n if (colour !== dereferenced) {\n errorMsg += ' (from \"' + colour + '\")';\n }\n throw Error(errorMsg);\n }\n }\n}\n","/**\n * @license\n * Copyright 2013 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * A div that floats on top of Blockly. This singleton contains\n * temporary HTML UI widgets that the user is currently interacting with.\n * E.g. text input areas, colour pickers, context menus.\n *\n * @namespace Blockly.WidgetDiv\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.WidgetDiv');\n\nimport * as common from './common.js';\nimport * as dom from './utils/dom.js';\nimport type {Rect} from './utils/rect.js';\nimport type {Size} from './utils/size.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\n/** The object currently using this container. */\nlet owner: unknown = null;\n\n/** Optional cleanup function set by whichever object uses the widget. */\nlet dispose: (() => void)|null = null;\n\n/** A class name representing the current owner's workspace renderer. */\nlet rendererClassName = '';\n\n/** A class name representing the current owner's workspace theme. */\nlet themeClassName = '';\n\n/** The HTML container for popup overlays (e.g. editor widgets). */\nlet containerDiv: HTMLDivElement|null;\n\n/**\n * Returns the HTML container for editor widgets.\n *\n * @returns The editor widget container.\n * @alias Blockly.WidgetDiv.getDiv\n */\nexport function getDiv(): HTMLDivElement|null {\n return containerDiv;\n}\n\n/**\n * Allows unit tests to reset the div. Do not use outside of tests.\n *\n * @param newDiv The new value for the DIV field.\n * @alias Blockly.WidgetDiv.testOnly_setDiv\n * @internal\n */\nexport function testOnly_setDiv(newDiv: HTMLDivElement|null) {\n containerDiv = newDiv;\n}\n\n/**\n * Create the widget div and inject it onto the page.\n *\n * @alias Blockly.WidgetDiv.createDom\n */\nexport function createDom() {\n if (containerDiv) {\n return; // Already created.\n }\n\n containerDiv = document.createElement('div') as HTMLDivElement;\n containerDiv.className = 'blocklyWidgetDiv';\n const container = common.getParentContainer() || document.body;\n container.appendChild(containerDiv);\n}\n\n/**\n * Initialize and display the widget div. Close the old one if needed.\n *\n * @param newOwner The object that will be using this container.\n * @param rtl Right-to-left (true) or left-to-right (false).\n * @param newDispose Optional cleanup function to be run when the widget is\n * closed.\n * @alias Blockly.WidgetDiv.show\n */\nexport function show(newOwner: unknown, rtl: boolean, newDispose: () => void) {\n hide();\n owner = newOwner;\n dispose = newDispose;\n const div = containerDiv;\n if (!div) return;\n div.style.direction = rtl ? 'rtl' : 'ltr';\n div.style.display = 'block';\n const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;\n rendererClassName = mainWorkspace.getRenderer().getClassName();\n themeClassName = mainWorkspace.getTheme().getClassName();\n if (rendererClassName) {\n dom.addClass(div, rendererClassName);\n }\n if (themeClassName) {\n dom.addClass(div, themeClassName);\n }\n}\n\n/**\n * Destroy the widget and hide the div.\n *\n * @alias Blockly.WidgetDiv.hide\n */\nexport function hide() {\n if (!isVisible()) {\n return;\n }\n owner = null;\n\n const div = containerDiv;\n if (!div) return;\n div.style.display = 'none';\n div.style.left = '';\n div.style.top = '';\n dispose && dispose();\n dispose = null;\n div.textContent = '';\n\n if (rendererClassName) {\n dom.removeClass(div, rendererClassName);\n rendererClassName = '';\n }\n if (themeClassName) {\n dom.removeClass(div, themeClassName);\n themeClassName = '';\n }\n (common.getMainWorkspace() as WorkspaceSvg).markFocused();\n}\n\n/**\n * Is the container visible?\n *\n * @returns True if visible.\n * @alias Blockly.WidgetDiv.isVisible\n */\nexport function isVisible(): boolean {\n return !!owner;\n}\n\n/**\n * Destroy the widget and hide the div if it is being used by the specified\n * object.\n *\n * @param oldOwner The object that was using this container.\n * @alias Blockly.WidgetDiv.hideIfOwner\n */\nexport function hideIfOwner(oldOwner: unknown) {\n if (owner === oldOwner) {\n hide();\n }\n}\n/**\n * Set the widget div's position and height. This function does nothing clever:\n * it will not ensure that your widget div ends up in the visible window.\n *\n * @param x Horizontal location (window coordinates, not body).\n * @param y Vertical location (window coordinates, not body).\n * @param height The height of the widget div (pixels).\n */\nfunction positionInternal(x: number, y: number, height: number) {\n containerDiv!.style.left = x + 'px';\n containerDiv!.style.top = y + 'px';\n containerDiv!.style.height = height + 'px';\n}\n\n/**\n * Position the widget div based on an anchor rectangle.\n * The widget should be placed adjacent to but not overlapping the anchor\n * rectangle. The preferred position is directly below and aligned to the left\n * (LTR) or right (RTL) side of the anchor.\n *\n * @param viewportBBox The bounding rectangle of the current viewport, in window\n * coordinates.\n * @param anchorBBox The bounding rectangle of the anchor, in window\n * coordinates.\n * @param widgetSize The size of the widget that is inside the widget div, in\n * window coordinates.\n * @param rtl Whether the workspace is in RTL mode. This determines horizontal\n * alignment.\n * @alias Blockly.WidgetDiv.positionWithAnchor\n * @internal\n */\nexport function positionWithAnchor(\n viewportBBox: Rect, anchorBBox: Rect, widgetSize: Size, rtl: boolean) {\n const y = calculateY(viewportBBox, anchorBBox, widgetSize);\n const x = calculateX(viewportBBox, anchorBBox, widgetSize, rtl);\n\n if (y < 0) {\n positionInternal(x, 0, widgetSize.height + y);\n } else {\n positionInternal(x, y, widgetSize.height);\n }\n}\n\n/**\n * Calculate an x position (in window coordinates) such that the widget will not\n * be offscreen on the right or left.\n *\n * @param viewportBBox The bounding rectangle of the current viewport, in window\n * coordinates.\n * @param anchorBBox The bounding rectangle of the anchor, in window\n * coordinates.\n * @param widgetSize The dimensions of the widget inside the widget div.\n * @param rtl Whether the Blockly workspace is in RTL mode.\n * @returns A valid x-coordinate for the top left corner of the widget div, in\n * window coordinates.\n */\nfunction calculateX(\n viewportBBox: Rect, anchorBBox: Rect, widgetSize: Size,\n rtl: boolean): number {\n if (rtl) {\n // Try to align the right side of the field and the right side of widget.\n const widgetLeft = anchorBBox.right - widgetSize.width;\n // Don't go offscreen left.\n const x = Math.max(widgetLeft, viewportBBox.left);\n // But really don't go offscreen right:\n return Math.min(x, viewportBBox.right - widgetSize.width);\n } else {\n // Try to align the left side of the field and the left side of widget.\n // Don't go offscreen right.\n const x = Math.min(anchorBBox.left, viewportBBox.right - widgetSize.width);\n // But left is more important, because that's where the text is.\n return Math.max(x, viewportBBox.left);\n }\n}\n\n/**\n * Calculate a y position (in window coordinates) such that the widget will not\n * be offscreen on the top or bottom.\n *\n * @param viewportBBox The bounding rectangle of the current viewport, in window\n * coordinates.\n * @param anchorBBox The bounding rectangle of the anchor, in window\n * coordinates.\n * @param widgetSize The dimensions of the widget inside the widget div.\n * @returns A valid y-coordinate for the top left corner of the widget div, in\n * window coordinates.\n */\nfunction calculateY(\n viewportBBox: Rect, anchorBBox: Rect, widgetSize: Size): number {\n // Flip the widget vertically if off the bottom.\n // The widget could go off the top of the window, but it would also go off\n // the bottom. The window is just too small.\n if (anchorBBox.bottom + widgetSize.height >= viewportBBox.bottom) {\n // The bottom of the widget is at the top of the field.\n return anchorBBox.top - widgetSize.height;\n } else {\n // The top of the widget is at the bottom of the field.\n return anchorBBox.bottom;\n }\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Fields can be created based on a JSON definition. This file\n * contains methods for registering those JSON definitions, and building the\n * fields based on JSON.\n *\n * @namespace Blockly.fieldRegistry\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.fieldRegistry');\n\nimport type {Field} from './field.js';\nimport type {IRegistrableField} from './interfaces/i_registrable_field.js';\nimport * as registry from './registry.js';\n\n\n/**\n * Registers a field type.\n * fieldRegistry.fromJson uses this registry to\n * find the appropriate field type.\n *\n * @param type The field type name as used in the JSON definition.\n * @param fieldClass The field class containing a fromJson function that can\n * construct an instance of the field.\n * @throws {Error} if the type name is empty, the field is already registered,\n * or the fieldClass is not an object containing a fromJson function.\n * @alias Blockly.fieldRegistry.register\n */\nexport function register(type: string, fieldClass: IRegistrableField) {\n registry.register(registry.Type.FIELD, type, fieldClass);\n}\n\n/**\n * Unregisters the field registered with the given type.\n *\n * @param type The field type name as used in the JSON definition.\n * @alias Blockly.fieldRegistry.unregister\n */\nexport function unregister(type: string) {\n registry.unregister(registry.Type.FIELD, type);\n}\n\n/**\n * Construct a Field from a JSON arg object.\n * Finds the appropriate registered field by the type name as registered using\n * fieldRegistry.register.\n *\n * @param options A JSON object with a type and options specific to the field\n * type.\n * @returns The new field instance or null if a field wasn't found with the\n * given type name\n * @alias Blockly.fieldRegistry.fromJson\n * @internal\n */\nexport function fromJson(options: AnyDuringMigration): Field|null {\n return TEST_ONLY.fromJsonInternal(options);\n}\n\n/**\n * Private version of fromJson for stubbing in tests.\n *\n * @param options\n */\nfunction fromJsonInternal(options: AnyDuringMigration): Field|null {\n const fieldObject = registry.getObject(registry.Type.FIELD, options['type']);\n if (!fieldObject) {\n console.warn(\n 'Blockly could not create a field of type ' + options['type'] +\n '. The field is probably not being registered. This could be because' +\n ' the file is not loaded, the field does not register itself (Issue' +\n ' #1584), or the registration is not being reached.');\n return null;\n } else if (typeof (fieldObject as any)['fromJson'] !== 'function') {\n throw new TypeError('returned Field was not a IRegistrableField');\n } else {\n return (fieldObject as unknown as IRegistrableField).fromJson(options);\n }\n}\n\nexport const TEST_ONLY = {\n fromJsonInternal,\n};\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * ARIA-related constants and utilities.\n * These methods are not specific to Blockly, and could be factored out into\n * a JavaScript framework such as Closure.\n *\n * @namespace Blockly.utils.aria\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.aria');\n\n\n/** ARIA states/properties prefix. */\nconst ARIA_PREFIX = 'aria-';\n\n/** ARIA role attribute. */\nconst ROLE_ATTRIBUTE = 'role';\n\n/**\n * ARIA role values.\n * Copied from Closure's goog.a11y.aria.Role\n *\n * @alias Blockly.utils.aria.Role\n */\nexport enum Role {\n // ARIA role for an interactive control of tabular data.\n GRID = 'grid',\n\n // ARIA role for a cell in a grid.\n GRIDCELL = 'gridcell',\n // ARIA role for a group of related elements like tree item siblings.\n GROUP = 'group',\n\n // ARIA role for a listbox.\n LISTBOX = 'listbox',\n\n // ARIA role for a popup menu.\n MENU = 'menu',\n\n // ARIA role for menu item elements.\n MENUITEM = 'menuitem',\n // ARIA role for a checkbox box element inside a menu.\n MENUITEMCHECKBOX = 'menuitemcheckbox',\n // ARIA role for option items that are children of combobox, listbox, menu,\n // radiogroup, or tree elements.\n OPTION = 'option',\n // ARIA role for ignorable cosmetic elements with no semantic significance.\n PRESENTATION = 'presentation',\n\n // ARIA role for a row of cells in a grid.\n ROW = 'row',\n // ARIA role for a tree.\n TREE = 'tree',\n\n // ARIA role for a tree item that sometimes may be expanded or collapsed.\n TREEITEM = 'treeitem'\n}\n\n/**\n * ARIA states and properties.\n * Copied from Closure's goog.a11y.aria.State\n *\n * @alias Blockly.utils.aria.State\n */\nexport enum State {\n // ARIA property for setting the currently active descendant of an element,\n // for example the selected item in a list box. Value: ID of an element.\n ACTIVEDESCENDANT = 'activedescendant',\n // ARIA property defines the total number of columns in a table, grid, or\n // treegrid.\n // Value: integer.\n COLCOUNT = 'colcount',\n // ARIA state for a disabled item. Value: one of {true, false}.\n DISABLED = 'disabled',\n\n // ARIA state for setting whether the element like a tree node is expanded.\n // Value: one of {true, false, undefined}.\n EXPANDED = 'expanded',\n\n // ARIA state indicating that the entered value does not conform. Value:\n // one of {false, true, 'grammar', 'spelling'}\n INVALID = 'invalid',\n\n // ARIA property that provides a label to override any other text, value, or\n // contents used to describe this element. Value: string.\n LABEL = 'label',\n // ARIA property for setting the element which labels another element.\n // Value: space-separated IDs of elements.\n LABELLEDBY = 'labelledby',\n\n // ARIA property for setting the level of an element in the hierarchy.\n // Value: integer.\n LEVEL = 'level',\n // ARIA property indicating if the element is horizontal or vertical.\n // Value: one of {'vertical', 'horizontal'}.\n ORIENTATION = 'orientation',\n\n // ARIA property that defines an element's number of position in a list.\n // Value: integer.\n POSINSET = 'posinset',\n\n // ARIA property defines the total number of rows in a table, grid, or\n // treegrid.\n // Value: integer.\n ROWCOUNT = 'rowcount',\n\n // ARIA state for setting the currently selected item in the list.\n // Value: one of {true, false, undefined}.\n SELECTED = 'selected',\n // ARIA property defining the number of items in a list. Value: integer.\n SETSIZE = 'setsize',\n\n // ARIA property for slider maximum value. Value: number.\n VALUEMAX = 'valuemax',\n\n // ARIA property for slider minimum value. Value: number.\n VALUEMIN = 'valuemin'\n}\n\n/**\n * Sets the role of an element.\n *\n * Similar to Closure's goog.a11y.aria\n *\n * @param element DOM node to set role of.\n * @param roleName Role name.\n * @alias Blockly.utils.aria.setRole\n */\nexport function setRole(element: Element, roleName: Role) {\n element.setAttribute(ROLE_ATTRIBUTE, roleName);\n}\n\n/**\n * Sets the state or property of an element.\n * Copied from Closure's goog.a11y.aria\n *\n * @param element DOM node where we set state.\n * @param stateName State attribute being set.\n * Automatically adds prefix 'aria-' to the state name if the attribute is\n * not an extra attribute.\n * @param value Value for the state attribute.\n * @alias Blockly.utils.aria.setState\n */\nexport function setState(\n element: Element, stateName: State, value: string|boolean|number|string[]) {\n if (Array.isArray(value)) {\n value = value.join(' ');\n }\n const attrStateName = ARIA_PREFIX + stateName;\n element.setAttribute(attrStateName, `${value}`);\n}\n","/**\n * @license\n * Copyright 2012 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Dropdown input field. Used for editable titles and variables.\n * In the interests of a consistent UI, the toolbox shares some functions and\n * properties with the context menu.\n *\n * @class\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.FieldDropdown');\n\nimport type {BlockSvg} from './block_svg.js';\nimport * as dropDownDiv from './dropdowndiv.js';\nimport {FieldConfig, Field} from './field.js';\nimport * as fieldRegistry from './field_registry.js';\nimport {Menu} from './menu.js';\nimport {MenuItem} from './menuitem.js';\nimport * as aria from './utils/aria.js';\nimport {Coordinate} from './utils/coordinate.js';\nimport * as dom from './utils/dom.js';\nimport * as parsing from './utils/parsing.js';\nimport type {Sentinel} from './utils/sentinel.js';\nimport * as utilsString from './utils/string.js';\nimport {Svg} from './utils/svg.js';\nimport * as userAgent from './utils/useragent.js';\n\n\n/**\n * Class for an editable dropdown field.\n *\n * @alias Blockly.FieldDropdown\n */\nexport class FieldDropdown extends Field {\n /** Horizontal distance that a checkmark overhangs the dropdown. */\n static CHECKMARK_OVERHANG = 25;\n\n /**\n * Maximum height of the dropdown menu, as a percentage of the viewport\n * height.\n */\n static MAX_MENU_HEIGHT_VH = 0.45;\n static ARROW_CHAR: AnyDuringMigration;\n\n /** A reference to the currently selected menu item. */\n private selectedMenuItem_: MenuItem|null = null;\n\n /** The dropdown menu. */\n protected menu_: Menu|null = null;\n\n /**\n * SVG image element if currently selected option is an image, or null.\n */\n private imageElement_: SVGImageElement|null = null;\n\n /** Tspan based arrow element. */\n private arrow_: SVGTSpanElement|null = null;\n\n /** SVG based arrow element. */\n private svgArrow_: SVGElement|null = null;\n\n /**\n * Serializable fields are saved by the serializer, non-serializable fields\n * are not. Editable fields should also be serializable.\n */\n override SERIALIZABLE = true;\n\n /** Mouse cursor style when over the hotspot that initiates the editor. */\n override CURSOR = 'default';\n // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.\n protected menuGenerator_!: AnyDuringMigration[][]|\n ((this: FieldDropdown) => AnyDuringMigration[][]);\n\n /** A cache of the most recently generated options. */\n // AnyDuringMigration because: Type 'null' is not assignable to type\n // 'string[][]'.\n private generatedOptions_: string[][] = null as AnyDuringMigration;\n\n /**\n * The prefix field label, of common words set after options are trimmed.\n *\n * @internal\n */\n override prefixField: string|null = null;\n\n /**\n * The suffix field label, of common words set after options are trimmed.\n *\n * @internal\n */\n override suffixField: string|null = null;\n // TODO(b/109816955): remove '!', see go/strict-prop-init-fix.\n private selectedOption_!: Array;\n override clickTarget_: AnyDuringMigration;\n\n /**\n * @param menuGenerator A non-empty array of options for a dropdown list, or a\n * function which generates these options. Also accepts Field.SKIP_SETUP\n * if you wish to skip setup (only used by subclasses that want to handle\n * configuration and setting the field value after their own constructors\n * have run).\n * @param opt_validator A function that is called to validate changes to the\n * field's value. Takes in a language-neutral dropdown option & returns a\n * validated language-neutral dropdown option, or null to abort the\n * change.\n * @param opt_config A map of options used to configure the field.\n * See the [field creation documentation]{@link\n * https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation}\n * for a list of properties this parameter supports.\n * @throws {TypeError} If `menuGenerator` options are incorrectly structured.\n */\n constructor(\n menuGenerator: AnyDuringMigration[][]|Function|Sentinel,\n opt_validator?: Function, opt_config?: FieldConfig) {\n super(Field.SKIP_SETUP);\n\n // If we pass SKIP_SETUP, don't do *anything* with the menu generator.\n if (menuGenerator === Field.SKIP_SETUP) {\n return;\n }\n\n if (Array.isArray(menuGenerator)) {\n validateOptions(menuGenerator);\n // Deep copy the option structure so it doesn't change.\n menuGenerator = JSON.parse(JSON.stringify(menuGenerator));\n }\n\n /**\n * An array of options for a dropdown list,\n * or a function which generates these options.\n */\n this.menuGenerator_ = menuGenerator as AnyDuringMigration[][] |\n ((this: FieldDropdown) => AnyDuringMigration[][]);\n\n this.trimOptions_();\n\n /**\n * The currently selected option. The field is initialized with the\n * first option selected.\n */\n this.selectedOption_ = this.getOptions(false)[0];\n\n if (opt_config) {\n this.configure_(opt_config);\n }\n this.setValue(this.selectedOption_[1]);\n if (opt_validator) {\n this.setValidator(opt_validator);\n }\n }\n\n /**\n * Sets the field's value based on the given XML element. Should only be\n * called by Blockly.Xml.\n *\n * @param fieldElement The element containing info about the field's state.\n * @internal\n */\n override fromXml(fieldElement: Element) {\n if (this.isOptionListDynamic()) {\n this.getOptions(false);\n }\n this.setValue(fieldElement.textContent);\n }\n\n /**\n * Sets the field's value based on the given state.\n *\n * @param state The state to apply to the dropdown field.\n * @internal\n */\n override loadState(state: AnyDuringMigration) {\n if (this.loadLegacyState(FieldDropdown, state)) {\n return;\n }\n if (this.isOptionListDynamic()) {\n this.getOptions(false);\n }\n this.setValue(state);\n }\n\n /**\n * Create the block UI for this dropdown.\n *\n * @internal\n */\n override initView() {\n if (this.shouldAddBorderRect_()) {\n this.createBorderRect_();\n } else {\n this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();\n }\n this.createTextElement_();\n\n this.imageElement_ = dom.createSvgElement(Svg.IMAGE, {}, this.fieldGroup_);\n\n if (this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW) {\n this.createSVGArrow_();\n } else {\n this.createTextArrow_();\n }\n\n if (this.borderRect_) {\n dom.addClass(this.borderRect_, 'blocklyDropdownRect');\n }\n }\n\n /**\n * Whether or not the dropdown should add a border rect.\n *\n * @returns True if the dropdown field should add a border rect.\n */\n protected shouldAddBorderRect_(): boolean {\n return !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||\n this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&\n !this.getSourceBlock().isShadow();\n }\n\n /** Create a tspan based arrow. */\n protected createTextArrow_() {\n this.arrow_ = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);\n this.arrow_!.appendChild(document.createTextNode(\n this.getSourceBlock().RTL ? FieldDropdown.ARROW_CHAR + ' ' :\n ' ' + FieldDropdown.ARROW_CHAR));\n if (this.getSourceBlock().RTL) {\n // AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'\n // is not assignable to parameter of type 'Node'.\n this.getTextElement().insertBefore(\n this.arrow_ as AnyDuringMigration, this.textContent_);\n } else {\n // AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'\n // is not assignable to parameter of type 'Node'.\n this.getTextElement().appendChild(this.arrow_ as AnyDuringMigration);\n }\n }\n\n /** Create an SVG based arrow. */\n protected createSVGArrow_() {\n this.svgArrow_ = dom.createSvgElement(\n Svg.IMAGE, {\n 'height': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',\n 'width': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',\n },\n this.fieldGroup_);\n this.svgArrow_!.setAttributeNS(\n dom.XLINK_NS, 'xlink:href',\n this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_DATAURI);\n }\n\n /**\n * Create a dropdown menu under the text.\n *\n * @param opt_e Optional mouse event that triggered the field to open, or\n * undefined if triggered programmatically.\n */\n protected override showEditor_(opt_e?: Event) {\n this.dropdownCreate_();\n // AnyDuringMigration because: Property 'clientX' does not exist on type\n // 'Event'.\n if (opt_e && typeof (opt_e as AnyDuringMigration).clientX === 'number') {\n // AnyDuringMigration because: Property 'clientY' does not exist on type\n // 'Event'. AnyDuringMigration because: Property 'clientX' does not exist\n // on type 'Event'.\n this.menu_!.openingCoords = new Coordinate(\n (opt_e as AnyDuringMigration).clientX,\n (opt_e as AnyDuringMigration).clientY);\n } else {\n this.menu_!.openingCoords = null;\n }\n\n // Remove any pre-existing elements in the dropdown.\n dropDownDiv.clearContent();\n // Element gets created in render.\n const menuElement = this.menu_!.render(dropDownDiv.getContentDiv());\n dom.addClass(menuElement, 'blocklyDropdownMenu');\n\n if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) {\n const primaryColour = this.getSourceBlock().isShadow() ?\n this.getSourceBlock().getParent()!.getColour() :\n this.getSourceBlock().getColour();\n const borderColour = this.getSourceBlock().isShadow() ?\n (this.getSourceBlock().getParent() as BlockSvg).style.colourTertiary :\n (this.sourceBlock_ as BlockSvg).style.colourTertiary;\n if (!borderColour) {\n throw new Error(\n 'The renderer did not properly initialize the block style');\n }\n dropDownDiv.setColour(primaryColour, borderColour);\n }\n\n dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));\n\n // Focusing needs to be handled after the menu is rendered and positioned.\n // Otherwise it will cause a page scroll to get the misplaced menu in\n // view. See issue #1329.\n this.menu_!.focus();\n\n if (this.selectedMenuItem_) {\n this.menu_!.setHighlighted(this.selectedMenuItem_);\n }\n\n this.applyColour();\n }\n\n /** Create the dropdown editor. */\n private dropdownCreate_() {\n const menu = new Menu();\n menu.setRole(aria.Role.LISTBOX);\n this.menu_ = menu;\n\n const options = this.getOptions(false);\n this.selectedMenuItem_ = null;\n for (let i = 0; i < options.length; i++) {\n let content = options[i][0]; // Human-readable text or image.\n const value = options[i][1]; // Language-neutral value.\n if (typeof content === 'object') {\n // An image, not text.\n const image = new Image(content['width'], content['height']);\n image.src = content['src'];\n image.alt = content['alt'] || '';\n content = image;\n }\n const menuItem = new MenuItem(content, value);\n menuItem.setRole(aria.Role.OPTION);\n menuItem.setRightToLeft(this.getSourceBlock().RTL);\n menuItem.setCheckable(true);\n menu.addChild(menuItem);\n menuItem.setChecked(value === this.value_);\n if (value === this.value_) {\n this.selectedMenuItem_ = menuItem;\n }\n menuItem.onAction(this.handleMenuActionEvent_, this);\n }\n }\n\n /**\n * Disposes of events and DOM-references belonging to the dropdown editor.\n */\n private dropdownDispose_() {\n if (this.menu_) {\n this.menu_.dispose();\n }\n this.menu_ = null;\n this.selectedMenuItem_ = null;\n this.applyColour();\n }\n\n /**\n * Handle an action in the dropdown menu.\n *\n * @param menuItem The MenuItem selected within menu.\n */\n private handleMenuActionEvent_(menuItem: MenuItem) {\n dropDownDiv.hideIfOwner(this, true);\n this.onItemSelected_(this.menu_ as Menu, menuItem);\n }\n\n /**\n * Handle the selection of an item in the dropdown menu.\n *\n * @param menu The Menu component clicked.\n * @param menuItem The MenuItem selected within menu.\n */\n protected onItemSelected_(menu: Menu, menuItem: MenuItem) {\n this.setValue(menuItem.getValue());\n }\n\n /**\n * Factor out common words in statically defined options.\n * Create prefix and/or suffix labels.\n */\n private trimOptions_() {\n const options = this.menuGenerator_;\n if (!Array.isArray(options)) {\n return;\n }\n let hasImages = false;\n\n // Localize label text and image alt text.\n for (let i = 0; i < options.length; i++) {\n const label = options[i][0];\n if (typeof label === 'string') {\n options[i][0] = parsing.replaceMessageReferences(label);\n } else {\n if (label.alt !== null) {\n options[i][0].alt = parsing.replaceMessageReferences(label.alt);\n }\n hasImages = true;\n }\n }\n if (hasImages || options.length < 2) {\n return; // Do nothing if too few items or at least one label is an image.\n }\n const strings = [];\n for (let i = 0; i < options.length; i++) {\n strings.push(options[i][0]);\n }\n const shortest = utilsString.shortestStringLength(strings);\n const prefixLength = utilsString.commonWordPrefix(strings, shortest);\n const suffixLength = utilsString.commonWordSuffix(strings, shortest);\n if (!prefixLength && !suffixLength) {\n return;\n }\n if (shortest <= prefixLength + suffixLength) {\n // One or more strings will entirely vanish if we proceed. Abort.\n return;\n }\n if (prefixLength) {\n this.prefixField = strings[0].substring(0, prefixLength - 1);\n }\n if (suffixLength) {\n this.suffixField = strings[0].substr(1 - suffixLength);\n }\n\n this.menuGenerator_ =\n FieldDropdown.applyTrim_(options, prefixLength, suffixLength);\n }\n\n /**\n * @returns True if the option list is generated by a function.\n * Otherwise false.\n */\n isOptionListDynamic(): boolean {\n return typeof this.menuGenerator_ === 'function';\n }\n\n /**\n * Return a list of the options for this dropdown.\n *\n * @param opt_useCache For dynamic options, whether or not to use the cached\n * options or to re-generate them.\n * @returns A non-empty array of option tuples:\n * (human-readable text or image, language-neutral name).\n * @throws {TypeError} If generated options are incorrectly structured.\n */\n getOptions(opt_useCache?: boolean): AnyDuringMigration[][] {\n if (this.isOptionListDynamic()) {\n if (!this.generatedOptions_ || !opt_useCache) {\n // AnyDuringMigration because: Property 'call' does not exist on type\n // 'any[][] | ((this: FieldDropdown) => any[][])'.\n this.generatedOptions_ =\n (this.menuGenerator_ as AnyDuringMigration).call(this);\n validateOptions(this.generatedOptions_);\n }\n return this.generatedOptions_;\n }\n return this.menuGenerator_ as string[][];\n }\n\n /**\n * Ensure that the input value is a valid language-neutral option.\n *\n * @param opt_newValue The input value.\n * @returns A valid language-neutral option, or null if invalid.\n */\n protected override doClassValidation_(opt_newValue?: AnyDuringMigration):\n string|null {\n let isValueValid = false;\n const options = this.getOptions(true);\n for (let i = 0, option; option = options[i]; i++) {\n // Options are tuples of human-readable text and language-neutral values.\n if (option[1] === opt_newValue) {\n isValueValid = true;\n break;\n }\n }\n if (!isValueValid) {\n if (this.sourceBlock_) {\n console.warn(\n 'Cannot set the dropdown\\'s value to an unavailable option.' +\n ' Block type: ' + this.sourceBlock_.type +\n ', Field name: ' + this.name + ', Value: ' + opt_newValue);\n }\n return null;\n }\n return opt_newValue as string;\n }\n\n /**\n * Update the value of this dropdown field.\n *\n * @param newValue The value to be saved. The default validator guarantees\n * that this is one of the valid dropdown options.\n */\n protected override doValueUpdate_(newValue: AnyDuringMigration) {\n super.doValueUpdate_(newValue);\n const options = this.getOptions(true);\n for (let i = 0, option; option = options[i]; i++) {\n if (option[1] === this.value_) {\n this.selectedOption_ = option;\n }\n }\n }\n\n /**\n * Updates the dropdown arrow to match the colour/style of the block.\n *\n * @internal\n */\n override applyColour() {\n const style = (this.sourceBlock_ as BlockSvg).style;\n if (!style.colourSecondary) {\n throw new Error(\n 'The renderer did not properly initialize the block style');\n }\n if (!style.colourTertiary) {\n throw new Error(\n 'The renderer did not properly initialize the block style');\n }\n if (this.borderRect_) {\n this.borderRect_.setAttribute('stroke', style.colourTertiary);\n if (this.menu_) {\n this.borderRect_.setAttribute('fill', style.colourTertiary);\n } else {\n this.borderRect_.setAttribute('fill', 'transparent');\n }\n }\n // Update arrow's colour.\n if (this.sourceBlock_ && this.arrow_) {\n if (this.sourceBlock_.isShadow()) {\n this.arrow_.style.fill = style.colourSecondary;\n } else {\n this.arrow_.style.fill = style.colourPrimary;\n }\n }\n }\n\n /** Draws the border with the correct width. */\n protected override render_() {\n // Hide both elements.\n this.getTextContent().nodeValue = '';\n this.imageElement_!.style.display = 'none';\n\n // Show correct element.\n const option = this.selectedOption_ && this.selectedOption_[0];\n if (option && typeof option === 'object') {\n this.renderSelectedImage_((option));\n } else {\n this.renderSelectedText_();\n }\n\n this.positionBorderRect_();\n }\n\n /**\n * Renders the selected option, which must be an image.\n *\n * @param imageJson Selected option that must be an image.\n */\n private renderSelectedImage_(imageJson: ImageProperties) {\n this.imageElement_!.style.display = '';\n this.imageElement_!.setAttributeNS(\n dom.XLINK_NS, 'xlink:href', imageJson.src);\n // AnyDuringMigration because: Argument of type 'number' is not assignable\n // to parameter of type 'string'.\n this.imageElement_!.setAttribute(\n 'height', imageJson.height as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type 'number' is not assignable\n // to parameter of type 'string'.\n this.imageElement_!.setAttribute(\n 'width', imageJson.width as AnyDuringMigration);\n\n const imageHeight = Number(imageJson.height);\n const imageWidth = Number(imageJson.width);\n\n // Height and width include the border rect.\n const hasBorder = !!this.borderRect_;\n const height = Math.max(\n hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,\n imageHeight + IMAGE_Y_PADDING);\n const xPadding =\n hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;\n let arrowWidth = 0;\n if (this.svgArrow_) {\n arrowWidth = this.positionSVGArrow_(\n imageWidth + xPadding,\n height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);\n } else {\n arrowWidth = dom.getFastTextWidth(\n this.arrow_ as SVGTSpanElement,\n this.getConstants()!.FIELD_TEXT_FONTSIZE,\n this.getConstants()!.FIELD_TEXT_FONTWEIGHT,\n this.getConstants()!.FIELD_TEXT_FONTFAMILY);\n }\n this.size_.width = imageWidth + arrowWidth + xPadding * 2;\n this.size_.height = height;\n\n let arrowX = 0;\n if (this.getSourceBlock().RTL) {\n const imageX = xPadding + arrowWidth;\n this.imageElement_!.setAttribute('x', imageX.toString());\n } else {\n arrowX = imageWidth + arrowWidth;\n this.getTextElement().setAttribute('text-anchor', 'end');\n this.imageElement_!.setAttribute('x', xPadding.toString());\n }\n this.imageElement_!.setAttribute(\n 'y', (height / 2 - imageHeight / 2).toString());\n\n this.positionTextElement_(arrowX + xPadding, imageWidth + arrowWidth);\n }\n\n /** Renders the selected option, which must be text. */\n private renderSelectedText_() {\n // Retrieves the selected option to display through getText_.\n this.getTextContent().nodeValue = this.getDisplayText_();\n const textElement = this.getTextElement();\n dom.addClass(textElement, 'blocklyDropdownText');\n textElement.setAttribute('text-anchor', 'start');\n\n // Height and width include the border rect.\n const hasBorder = !!this.borderRect_;\n const height = Math.max(\n hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,\n this.getConstants()!.FIELD_TEXT_HEIGHT);\n const textWidth = dom.getFastTextWidth(\n this.getTextElement(), this.getConstants()!.FIELD_TEXT_FONTSIZE,\n this.getConstants()!.FIELD_TEXT_FONTWEIGHT,\n this.getConstants()!.FIELD_TEXT_FONTFAMILY);\n const xPadding =\n hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;\n let arrowWidth = 0;\n if (this.svgArrow_) {\n arrowWidth = this.positionSVGArrow_(\n textWidth + xPadding,\n height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);\n }\n this.size_.width = textWidth + arrowWidth + xPadding * 2;\n this.size_.height = height;\n\n this.positionTextElement_(xPadding, textWidth);\n }\n\n /**\n * Position a drop-down arrow at the appropriate location at render-time.\n *\n * @param x X position the arrow is being rendered at, in px.\n * @param y Y position the arrow is being rendered at, in px.\n * @returns Amount of space the arrow is taking up, in px.\n */\n private positionSVGArrow_(x: number, y: number): number {\n if (!this.svgArrow_) {\n return 0;\n }\n const hasBorder = !!this.borderRect_;\n const xPadding =\n hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;\n const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING;\n const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE;\n const arrowX = this.getSourceBlock().RTL ? xPadding : x + textPadding;\n this.svgArrow_.setAttribute(\n 'transform', 'translate(' + arrowX + ',' + y + ')');\n return svgArrowSize + textPadding;\n }\n\n /**\n * Use the `getText_` developer hook to override the field's text\n * representation. Get the selected option text. If the selected option is an\n * image we return the image alt text.\n *\n * @returns Selected option text.\n */\n protected override getText_(): string|null {\n if (!this.selectedOption_) {\n return null;\n }\n const option = this.selectedOption_[0];\n if (typeof option === 'object') {\n return option['alt'];\n }\n return option;\n }\n\n /**\n * Construct a FieldDropdown from a JSON arg object.\n *\n * @param options A JSON object with options (options).\n * @returns The new field instance.\n * @nocollapse\n * @internal\n */\n static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown {\n if (!options.options) {\n throw new Error(\n 'options are required for the dropdown field. The ' +\n 'options property must be assigned an array of ' +\n '[humanReadableValue, languageNeutralValue] tuples.');\n }\n // `this` might be a subclass of FieldDropdown if that class doesn't\n // override the static fromJson method.\n return new this(options.options, undefined, options);\n }\n\n /**\n * Use the calculated prefix and suffix lengths to trim all of the options in\n * the given array.\n *\n * @param options Array of option tuples:\n * (human-readable text or image, language-neutral name).\n * @param prefixLength The length of the common prefix.\n * @param suffixLength The length of the common suffix\n * @returns A new array with all of the option text trimmed.\n */\n static applyTrim_(\n options: AnyDuringMigration[][], prefixLength: number,\n suffixLength: number): AnyDuringMigration[][] {\n const newOptions = [];\n // Remove the prefix and suffix from the options.\n for (let i = 0; i < options.length; i++) {\n let text = options[i][0];\n const value = options[i][1];\n text = text.substring(prefixLength, text.length - suffixLength);\n newOptions[i] = [text, value];\n }\n return newOptions;\n }\n}\n\n/**\n * Definition of a human-readable image dropdown option.\n */\nexport interface ImageProperties {\n src: string;\n alt: string;\n width: number;\n height: number;\n}\n\n/**\n * An individual option in the dropdown menu. The first element is the human-\n * readable value (text or image), and the second element is the language-\n * neutral value.\n */\nexport type MenuOption = [string | ImageProperties, string];\n\n/**\n * fromJson config for the dropdown field.\n */\nexport interface FieldDropdownFromJsonConfig extends FieldConfig {\n options?: MenuOption[];\n}\n\n/**\n * The y offset from the top of the field to the top of the image, if an image\n * is selected.\n */\nconst IMAGE_Y_OFFSET = 5;\n\n/** The total vertical padding above and below an image. */\nconst IMAGE_Y_PADDING: number = IMAGE_Y_OFFSET * 2;\n\n/** Android can't (in 2014) display \"▾\", so use \"▼\" instead. */\nFieldDropdown.ARROW_CHAR = userAgent.ANDROID ? '▼' : '▾';\n\n/**\n * Validates the data structure to be processed as an options list.\n *\n * @param options The proposed dropdown options.\n * @throws {TypeError} If proposed options are incorrectly structured.\n */\nfunction validateOptions(options: AnyDuringMigration) {\n if (!Array.isArray(options)) {\n throw TypeError('FieldDropdown options must be an array.');\n }\n if (!options.length) {\n throw TypeError('FieldDropdown options must not be an empty array.');\n }\n let foundError = false;\n for (let i = 0; i < options.length; i++) {\n const tuple = options[i];\n if (!Array.isArray(tuple)) {\n foundError = true;\n console.error(\n 'Invalid option[' + i + ']: Each FieldDropdown option must be an ' +\n 'array. Found: ',\n tuple);\n } else if (typeof tuple[1] !== 'string') {\n foundError = true;\n console.error(\n 'Invalid option[' + i + ']: Each FieldDropdown option id must be ' +\n 'a string. Found ' + tuple[1] + ' in: ',\n tuple);\n } else if (\n tuple[0] && typeof tuple[0] !== 'string' &&\n typeof tuple[0].src !== 'string') {\n foundError = true;\n console.error(\n 'Invalid option[' + i + ']: Each FieldDropdown option must have a ' +\n 'string label or image description. Found' + tuple[0] + ' in: ',\n tuple);\n }\n }\n if (foundError) {\n throw TypeError('Found invalid FieldDropdown options.');\n }\n}\n\nfieldRegistry.register('field_dropdown', FieldDropdown);\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods for objects.\n *\n * @namespace Blockly.utils.object\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.object');\n\nimport * as deprecation from './deprecation.js';\n\n\n/**\n * Inherit the prototype methods from one constructor into another.\n *\n * @param childCtor Child class.\n * @param parentCtor Parent class.\n * @suppress {strictMissingProperties} superClass_ is not defined on Function.\n * @deprecated No longer provided by Blockly.\n * @alias Blockly.utils.object.inherits\n */\nexport function inherits(childCtor: Function, parentCtor: Function) {\n deprecation.warn('Blockly.utils.object.inherits', 'version 9', 'version 10');\n // Set a .superClass_ property so that methods can call parent methods\n // without hard-coding the parent class name.\n // Could be replaced by ES6's super().\n // AnyDuringMigration because: Property 'superClass_' does not exist on type\n // 'Function'.\n (childCtor as AnyDuringMigration).superClass_ = parentCtor.prototype;\n\n // Link the child class to the parent class so that static methods inherit.\n Object.setPrototypeOf(childCtor, parentCtor);\n\n // Replace the child constructor's prototype object with an instance\n // of the parent class.\n childCtor.prototype = Object.create(parentCtor.prototype);\n childCtor.prototype.constructor = childCtor;\n}\n// Alternatively, one could use this instead:\n// Object.setPrototypeOf(childCtor.prototype, parentCtor.prototype);\n\n/**\n * Copies all the members of a source object to a target object.\n *\n * @param target Target.\n * @param source Source.\n * @deprecated Use the built-in **Object.assign** instead.\n * @alias Blockly.utils.object.mixin\n */\nexport function mixin(target: AnyDuringMigration, source: AnyDuringMigration) {\n deprecation.warn(\n 'Blockly.utils.object.mixin', 'May 2022', 'May 2023', 'Object.assign');\n for (const x in source) {\n target[x] = source[x];\n }\n}\n\n/**\n * Complete a deep merge of all members of a source object with a target object.\n *\n * @param target Target.\n * @param source Source.\n * @returns The resulting object.\n * @alias Blockly.utils.object.deepMerge\n */\nexport function deepMerge(\n target: AnyDuringMigration,\n source: AnyDuringMigration): AnyDuringMigration {\n for (const x in source) {\n if (source[x] !== null && typeof source[x] === 'object') {\n target[x] = deepMerge(target[x] || Object.create(null), source[x]);\n } else {\n target[x] = source[x];\n }\n }\n return target;\n}\n\n/**\n * Returns an array of a given object's own enumerable property values.\n *\n * @param obj Object containing values.\n * @returns Array of values.\n * @deprecated Use the built-in **Object.values** instead.\n * @alias Blockly.utils.object.values\n */\nexport function values(obj: AnyDuringMigration): AnyDuringMigration[] {\n deprecation.warn(\n 'Blockly.utils.object.values', 'version 9', 'version 10',\n 'Object.values');\n return Object.values(obj);\n}\n","/**\n * @license\n * Copyright 2020 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility functions for the toolbox and flyout.\n *\n * @namespace Blockly.utils.toolbox\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.toolbox');\n\nimport type {ConnectionState} from '../serialization/blocks.js';\nimport type {CssConfig as CategoryCssConfig} from '../toolbox/category.js';\nimport type {CssConfig as SeparatorCssConfig} from '../toolbox/separator.js';\nimport * as Xml from '../xml.js';\n\n\n/**\n * The information needed to create a block in the toolbox.\n * Note that disabled has a different type for backwards compatibility.\n *\n * @alias Blockly.utils.toolbox.BlockInfo\n */\nexport interface BlockInfo {\n kind: string;\n blockxml?: string|Node;\n type?: string;\n gap?: string|number;\n disabled?: string|boolean;\n enabled?: boolean;\n id?: string;\n x?: number;\n y?: number;\n collapsed?: boolean;\n inline?: boolean;\n data?: string;\n extraState?: AnyDuringMigration;\n icons?: {[key: string]: AnyDuringMigration};\n fields?: {[key: string]: AnyDuringMigration};\n inputs?: {[key: string]: ConnectionState};\n next?: ConnectionState;\n}\n\n/**\n * The information needed to create a separator in the toolbox.\n *\n * @alias Blockly.utils.toolbox.SeparatorInfo\n */\nexport interface SeparatorInfo {\n kind: string;\n id: string|undefined;\n gap: number|undefined;\n cssconfig: SeparatorCssConfig|undefined;\n}\n\n/**\n * The information needed to create a button in the toolbox.\n *\n * @alias Blockly.utils.toolbox.ButtonInfo\n */\nexport interface ButtonInfo {\n kind: string;\n text: string;\n callbackkey: string;\n}\n\n/**\n * The information needed to create a label in the toolbox.\n *\n * @alias Blockly.utils.toolbox.LabelInfo\n */\nexport interface LabelInfo {\n kind: string;\n text: string;\n id: string|undefined;\n}\n\n/**\n * The information needed to create either a button or a label in the flyout.\n *\n * @alias Blockly.utils.toolbox.ButtonOrLabelInfo\n */\nexport type ButtonOrLabelInfo = ButtonInfo|LabelInfo;\n\n/**\n * The information needed to create a category in the toolbox.\n *\n * @alias Blockly.utils.toolbox.StaticCategoryInfo\n */\nexport interface StaticCategoryInfo {\n kind: string;\n name: string;\n contents: ToolboxItemInfo[];\n id: string|undefined;\n categorystyle: string|undefined;\n colour: string|undefined;\n cssconfig: CategoryCssConfig|undefined;\n hidden: string|undefined;\n}\n\n/**\n * The information needed to create a custom category.\n *\n * @alias Blockly.utils.toolbox.DynamicCategoryInfo\n */\nexport interface DynamicCategoryInfo {\n kind: string;\n custom: string;\n id: string|undefined;\n categorystyle: string|undefined;\n colour: string|undefined;\n cssconfig: CategoryCssConfig|undefined;\n hidden: string|undefined;\n}\n\n/**\n * The information needed to create either a dynamic or static category.\n *\n * @alias Blockly.utils.toolbox.CategoryInfo\n */\nexport type CategoryInfo = StaticCategoryInfo|DynamicCategoryInfo;\n\n/**\n * Any information that can be used to create an item in the toolbox.\n *\n * @alias Blockly.utils.toolbox.ToolboxItemInfo\n */\nexport type ToolboxItemInfo = FlyoutItemInfo|StaticCategoryInfo;\n\n/**\n * All the different types that can be displayed in a flyout.\n *\n * @alias Blockly.utils.toolbox.FlyoutItemInfo\n */\nexport type FlyoutItemInfo =\n BlockInfo|SeparatorInfo|ButtonInfo|LabelInfo|DynamicCategoryInfo;\n\n/**\n * The JSON definition of a toolbox.\n *\n * @alias Blockly.utils.toolbox.ToolboxInfo\n */\nexport interface ToolboxInfo {\n kind?: string;\n contents: ToolboxItemInfo[];\n}\n\n/**\n * An array holding flyout items.\n *\n * @alias Blockly.utils.toolbox.FlyoutItemInfoArray\n */\nexport type FlyoutItemInfoArray = FlyoutItemInfo[];\n\n/**\n * All of the different types that can create a toolbox.\n *\n * @alias Blockly.utils.toolbox.ToolboxDefinition\n */\nexport type ToolboxDefinition = Node|ToolboxInfo|string;\n\n/**\n * All of the different types that can be used to show items in a flyout.\n *\n * @alias Blockly.utils.toolbox.FlyoutDefinition\n */\nexport type FlyoutDefinition = FlyoutItemInfoArray|NodeList|ToolboxInfo|Node[];\n\n/**\n * The name used to identify a toolbox that has category like items.\n * This only needs to be used if a toolbox wants to be treated like a category\n * toolbox but does not actually contain any toolbox items with the kind\n * 'category'.\n */\nconst CATEGORY_TOOLBOX_KIND = 'categoryToolbox';\n\n/**\n * The name used to identify a toolbox that has no categories and is displayed\n * as a simple flyout displaying blocks, buttons, or labels.\n */\nconst FLYOUT_TOOLBOX_KIND = 'flyoutToolbox';\n\n/**\n * Position of the toolbox and/or flyout relative to the workspace.\n *\n * @alias Blockly.utils.toolbox.Position\n */\nexport enum Position {\n TOP,\n BOTTOM,\n LEFT,\n RIGHT\n}\n\n/**\n * Converts the toolbox definition into toolbox JSON.\n *\n * @param toolboxDef The definition of the toolbox in one of its many forms.\n * @returns Object holding information for creating a toolbox.\n * @alias Blockly.utils.toolbox.convertToolboxDefToJson\n * @internal\n */\nexport function convertToolboxDefToJson(toolboxDef: ToolboxDefinition|\n null): ToolboxInfo|null {\n if (!toolboxDef) {\n return null;\n }\n\n if (toolboxDef instanceof Element || typeof toolboxDef === 'string') {\n toolboxDef = parseToolboxTree(toolboxDef);\n // AnyDuringMigration because: Argument of type 'Node | null' is not\n // assignable to parameter of type 'Node'.\n toolboxDef = convertToToolboxJson(toolboxDef as AnyDuringMigration);\n }\n\n const toolboxJson = toolboxDef as ToolboxInfo;\n validateToolbox(toolboxJson);\n return toolboxJson;\n}\n\n/**\n * Validates the toolbox JSON fields have been set correctly.\n *\n * @param toolboxJson Object holding information for creating a toolbox.\n * @throws {Error} if the toolbox is not the correct format.\n */\nfunction validateToolbox(toolboxJson: ToolboxInfo) {\n const toolboxKind = toolboxJson['kind'];\n const toolboxContents = toolboxJson['contents'];\n\n if (toolboxKind) {\n if (toolboxKind !== FLYOUT_TOOLBOX_KIND &&\n toolboxKind !== CATEGORY_TOOLBOX_KIND) {\n throw Error(\n 'Invalid toolbox kind ' + toolboxKind + '.' +\n ' Please supply either ' + FLYOUT_TOOLBOX_KIND + ' or ' +\n CATEGORY_TOOLBOX_KIND);\n }\n }\n if (!toolboxContents) {\n throw Error('Toolbox must have a contents attribute.');\n }\n}\n\n/**\n * Converts the flyout definition into a list of flyout items.\n *\n * @param flyoutDef The definition of the flyout in one of its many forms.\n * @returns A list of flyout items.\n * @alias Blockly.utils.toolbox.convertFlyoutDefToJsonArray\n * @internal\n */\nexport function convertFlyoutDefToJsonArray(flyoutDef: FlyoutDefinition|\n null): FlyoutItemInfoArray {\n if (!flyoutDef) {\n return [];\n }\n\n if ((flyoutDef as AnyDuringMigration)['contents']) {\n return (flyoutDef as AnyDuringMigration)['contents'];\n }\n // If it is already in the correct format return the flyoutDef.\n // AnyDuringMigration because: Property 'nodeType' does not exist on type\n // 'Node | FlyoutItemInfo'.\n if (Array.isArray(flyoutDef) && flyoutDef.length > 0 &&\n !((flyoutDef[0]) as AnyDuringMigration).nodeType) {\n // AnyDuringMigration because: Type 'FlyoutItemInfoArray | Node[]' is not\n // assignable to type 'FlyoutItemInfoArray'.\n return flyoutDef as AnyDuringMigration;\n }\n\n // AnyDuringMigration because: Type 'ToolboxItemInfo[] | FlyoutItemInfoArray'\n // is not assignable to type 'FlyoutItemInfoArray'.\n return xmlToJsonArray(flyoutDef as Node[] | NodeList) as AnyDuringMigration;\n}\n\n/**\n * Whether or not the toolbox definition has categories.\n *\n * @param toolboxJson Object holding information for creating a toolbox.\n * @returns True if the toolbox has categories.\n * @alias Blockly.utils.toolbox.hasCategories\n * @internal\n */\nexport function hasCategories(toolboxJson: ToolboxInfo|null): boolean {\n return TEST_ONLY.hasCategoriesInternal(toolboxJson);\n}\n\n/**\n * Private version of hasCategories for stubbing in tests.\n */\nfunction hasCategoriesInternal(toolboxJson: ToolboxInfo|null): boolean {\n if (!toolboxJson) {\n return false;\n }\n\n const toolboxKind = toolboxJson['kind'];\n if (toolboxKind) {\n return toolboxKind === CATEGORY_TOOLBOX_KIND;\n }\n\n const categories = toolboxJson['contents'].filter(function(item) {\n return item['kind'].toUpperCase() === 'CATEGORY';\n });\n return !!categories.length;\n}\n\n/**\n * Whether or not the category is collapsible.\n *\n * @param categoryInfo Object holing information for creating a category.\n * @returns True if the category has subcategories.\n * @alias Blockly.utils.toolbox.isCategoryCollapsible\n * @internal\n */\nexport function isCategoryCollapsible(categoryInfo: CategoryInfo): boolean {\n if (!categoryInfo || !(categoryInfo as AnyDuringMigration)['contents']) {\n return false;\n }\n\n const categories =\n (categoryInfo as AnyDuringMigration)['contents'].filter(function(\n item: AnyDuringMigration) {\n return item['kind'].toUpperCase() === 'CATEGORY';\n });\n return !!categories.length;\n}\n\n/**\n * Parses the provided toolbox definition into a consistent format.\n *\n * @param toolboxDef The definition of the toolbox in one of its many forms.\n * @returns Object holding information for creating a toolbox.\n */\nfunction convertToToolboxJson(toolboxDef: Node): ToolboxInfo {\n const contents = xmlToJsonArray(toolboxDef as Node | Node[]);\n const toolboxJson = {'contents': contents};\n if (toolboxDef instanceof Node) {\n addAttributes(toolboxDef, toolboxJson);\n }\n return toolboxJson;\n}\n\n/**\n * Converts the xml for a toolbox to JSON.\n *\n * @param toolboxDef The definition of the toolbox in one of its many forms.\n * @returns A list of objects in the toolbox.\n */\nfunction xmlToJsonArray(toolboxDef: Node|Node[]|NodeList): FlyoutItemInfoArray|\n ToolboxItemInfo[] {\n const arr = [];\n // If it is a node it will have children.\n // AnyDuringMigration because: Property 'childNodes' does not exist on type\n // 'Node | NodeList | Node[]'.\n let childNodes = (toolboxDef as AnyDuringMigration).childNodes;\n if (!childNodes) {\n // Otherwise the toolboxDef is an array or collection.\n childNodes = toolboxDef;\n }\n for (let i = 0, child; child = childNodes[i]; i++) {\n if (!child.tagName) {\n continue;\n }\n const obj = {};\n const tagName = child.tagName.toUpperCase();\n (obj as AnyDuringMigration)['kind'] = tagName;\n\n // Store the XML for a block.\n if (tagName === 'BLOCK') {\n (obj as AnyDuringMigration)['blockxml'] = child;\n } else if (child.childNodes && child.childNodes.length > 0) {\n // Get the contents of a category\n (obj as AnyDuringMigration)['contents'] = xmlToJsonArray(child);\n }\n\n // Add XML attributes to object\n addAttributes(child, obj);\n arr.push(obj);\n }\n // AnyDuringMigration because: Type '{}[]' is not assignable to type\n // 'ToolboxItemInfo[] | FlyoutItemInfoArray'.\n return arr as AnyDuringMigration;\n}\n\n/**\n * Adds the attributes on the node to the given object.\n *\n * @param node The node to copy the attributes from.\n * @param obj The object to copy the attributes to.\n */\nfunction addAttributes(node: Node, obj: AnyDuringMigration) {\n // AnyDuringMigration because: Property 'attributes' does not exist on type\n // 'Node'.\n for (let j = 0; j < (node as AnyDuringMigration).attributes.length; j++) {\n // AnyDuringMigration because: Property 'attributes' does not exist on type\n // 'Node'.\n const attr = (node as AnyDuringMigration).attributes[j];\n if (attr.nodeName.indexOf('css-') > -1) {\n obj['cssconfig'] = obj['cssconfig'] || {};\n obj['cssconfig'][attr.nodeName.replace('css-', '')] = attr.value;\n } else {\n obj[attr.nodeName] = attr.value;\n }\n }\n}\n\n/**\n * Parse the provided toolbox tree into a consistent DOM format.\n *\n * @param toolboxDef DOM tree of blocks, or text representation of same.\n * @returns DOM tree of blocks, or null.\n * @alias Blockly.utils.toolbox.parseToolboxTree\n */\nexport function parseToolboxTree(toolboxDef: Element|null|string): Element|\n null {\n let parsedToolboxDef: Element|null = null;\n if (toolboxDef) {\n if (typeof toolboxDef === 'string') {\n parsedToolboxDef = Xml.textToDom(toolboxDef);\n if (parsedToolboxDef.nodeName.toLowerCase() !== 'xml') {\n throw TypeError('Toolbox should be an document.');\n }\n } else if (toolboxDef instanceof Element) {\n parsedToolboxDef = toolboxDef;\n }\n }\n return parsedToolboxDef;\n}\n\nexport const TEST_ONLY = {\n hasCategoriesInternal,\n};\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Extensions are functions that help initialize blocks, usually\n * adding dynamic behavior such as onchange handlers and mutators. These\n * are applied using Block.applyExtension(), or the JSON \"extensions\"\n * array attribute.\n *\n * @namespace Blockly.Extensions\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Extensions');\n\nimport type {Block} from './block.js';\nimport type {BlockSvg} from './block_svg.js';\nimport {FieldDropdown} from './field_dropdown.js';\nimport {Mutator} from './mutator.js';\nimport * as parsing from './utils/parsing.js';\n\n\n/** The set of all registered extensions, keyed by extension name/id. */\nconst allExtensions = Object.create(null);\nexport const TEST_ONLY = {allExtensions};\n\n/**\n * Registers a new extension function. Extensions are functions that help\n * initialize blocks, usually adding dynamic behavior such as onchange\n * handlers and mutators. These are applied using Block.applyExtension(), or\n * the JSON \"extensions\" array attribute.\n *\n * @param name The name of this extension.\n * @param initFn The function to initialize an extended block.\n * @throws {Error} if the extension name is empty, the extension is already\n * registered, or extensionFn is not a function.\n * @alias Blockly.Extensions.register\n */\nexport function register(name: string, initFn: Function) {\n if (typeof name !== 'string' || name.trim() === '') {\n throw Error('Error: Invalid extension name \"' + name + '\"');\n }\n if (allExtensions[name]) {\n throw Error('Error: Extension \"' + name + '\" is already registered.');\n }\n if (typeof initFn !== 'function') {\n throw Error('Error: Extension \"' + name + '\" must be a function');\n }\n allExtensions[name] = initFn;\n}\n\n/**\n * Registers a new extension function that adds all key/value of mixinObj.\n *\n * @param name The name of this extension.\n * @param mixinObj The values to mix in.\n * @throws {Error} if the extension name is empty or the extension is already\n * registered.\n * @alias Blockly.Extensions.registerMixin\n */\nexport function registerMixin(name: string, mixinObj: AnyDuringMigration) {\n if (!mixinObj || typeof mixinObj !== 'object') {\n throw Error('Error: Mixin \"' + name + '\" must be a object');\n }\n register(name, function(this: Block) {\n this.mixin(mixinObj);\n });\n}\n\n/**\n * Registers a new extension function that adds a mutator to the block.\n * At register time this performs some basic sanity checks on the mutator.\n * The wrapper may also add a mutator dialog to the block, if both compose and\n * decompose are defined on the mixin.\n *\n * @param name The name of this mutator extension.\n * @param mixinObj The values to mix in.\n * @param opt_helperFn An optional function to apply after mixing in the object.\n * @param opt_blockList A list of blocks to appear in the flyout of the mutator\n * dialog.\n * @throws {Error} if the mutation is invalid or can't be applied to the block.\n * @alias Blockly.Extensions.registerMutator\n */\nexport function registerMutator(\n name: string, mixinObj: AnyDuringMigration,\n opt_helperFn?: () => AnyDuringMigration, opt_blockList?: string[]) {\n const errorPrefix = 'Error when registering mutator \"' + name + '\": ';\n\n checkHasMutatorProperties(errorPrefix, mixinObj);\n const hasMutatorDialog = checkMutatorDialog(mixinObj, errorPrefix);\n\n if (opt_helperFn && typeof opt_helperFn !== 'function') {\n throw Error(errorPrefix + 'Extension \"' + name + '\" is not a function');\n }\n\n // Sanity checks passed.\n register(name, function(this: Block) {\n if (hasMutatorDialog) {\n this.setMutator(new Mutator(opt_blockList || [], this as BlockSvg));\n }\n // Mixin the object.\n this.mixin(mixinObj);\n\n if (opt_helperFn) {\n opt_helperFn.apply(this);\n }\n });\n}\n\n/**\n * Unregisters the extension registered with the given name.\n *\n * @param name The name of the extension to unregister.\n * @alias Blockly.Extensions.unregister\n */\nexport function unregister(name: string) {\n if (isRegistered(name)) {\n delete allExtensions[name];\n } else {\n console.warn(\n 'No extension mapping for name \"' + name + '\" found to unregister');\n }\n}\n\n/**\n * Returns whether an extension is registered with the given name.\n *\n * @param name The name of the extension to check for.\n * @returns True if the extension is registered. False if it is not registered.\n * @alias Blockly.Extensions.isRegistered\n */\nexport function isRegistered(name: string): boolean {\n return !!allExtensions[name];\n}\n\n/**\n * Applies an extension method to a block. This should only be called during\n * block construction.\n *\n * @param name The name of the extension.\n * @param block The block to apply the named extension to.\n * @param isMutator True if this extension defines a mutator.\n * @throws {Error} if the extension is not found.\n * @alias Blockly.Extensions.apply\n */\nexport function apply(name: string, block: Block, isMutator: boolean) {\n const extensionFn = allExtensions[name];\n if (typeof extensionFn !== 'function') {\n throw Error('Error: Extension \"' + name + '\" not found.');\n }\n let mutatorProperties;\n if (isMutator) {\n // Fail early if the block already has mutation properties.\n checkNoMutatorProperties(name, block);\n } else {\n // Record the old properties so we can make sure they don't change after\n // applying the extension.\n mutatorProperties = getMutatorProperties(block);\n }\n extensionFn.apply(block);\n\n if (isMutator) {\n const errorPrefix = 'Error after applying mutator \"' + name + '\": ';\n checkHasMutatorProperties(errorPrefix, block);\n } else {\n if (!mutatorPropertiesMatch(\n mutatorProperties as AnyDuringMigration[], block)) {\n throw Error(\n 'Error when applying extension \"' + name + '\": ' +\n 'mutation properties changed when applying a non-mutator extension.');\n }\n }\n}\n\n/**\n * Check that the given block does not have any of the four mutator properties\n * defined on it. This function should be called before applying a mutator\n * extension to a block, to make sure we are not overwriting properties.\n *\n * @param mutationName The name of the mutation to reference in error messages.\n * @param block The block to check.\n * @throws {Error} if any of the properties already exist on the block.\n */\nfunction checkNoMutatorProperties(mutationName: string, block: Block) {\n const properties = getMutatorProperties(block);\n if (properties.length) {\n throw Error(\n 'Error: tried to apply mutation \"' + mutationName +\n '\" to a block that already has mutator functions.' +\n ' Block id: ' + block.id);\n }\n}\n\n/**\n * Checks if the given object has both the 'mutationToDom' and 'domToMutation'\n * functions.\n *\n * @param object The object to check.\n * @param errorPrefix The string to prepend to any error message.\n * @returns True if the object has both functions. False if it has neither\n * function.\n * @throws {Error} if the object has only one of the functions, or either is not\n * actually a function.\n */\nfunction checkXmlHooks(\n object: AnyDuringMigration, errorPrefix: string): boolean {\n return checkHasFunctionPair(\n object.mutationToDom, object.domToMutation,\n errorPrefix + ' mutationToDom/domToMutation');\n}\n/**\n * Checks if the given object has both the 'saveExtraState' and 'loadExtraState'\n * functions.\n *\n * @param object The object to check.\n * @param errorPrefix The string to prepend to any error message.\n * @returns True if the object has both functions. False if it has neither\n * function.\n * @throws {Error} if the object has only one of the functions, or either is not\n * actually a function.\n */\nfunction checkJsonHooks(\n object: AnyDuringMigration, errorPrefix: string): boolean {\n return checkHasFunctionPair(\n object.saveExtraState, object.loadExtraState,\n errorPrefix + ' saveExtraState/loadExtraState');\n}\n\n/**\n * Checks if the given object has both the 'compose' and 'decompose' functions.\n *\n * @param object The object to check.\n * @param errorPrefix The string to prepend to any error message.\n * @returns True if the object has both functions. False if it has neither\n * function.\n * @throws {Error} if the object has only one of the functions, or either is not\n * actually a function.\n */\nfunction checkMutatorDialog(\n object: AnyDuringMigration, errorPrefix: string): boolean {\n return checkHasFunctionPair(\n object.compose, object.decompose, errorPrefix + ' compose/decompose');\n}\n\n/**\n * Checks that both or neither of the given functions exist and that they are\n * indeed functions.\n *\n * @param func1 The first function in the pair.\n * @param func2 The second function in the pair.\n * @param errorPrefix The string to prepend to any error message.\n * @returns True if the object has both functions. False if it has neither\n * function.\n * @throws {Error} If the object has only one of the functions, or either is not\n * actually a function.\n */\nfunction checkHasFunctionPair(\n func1: AnyDuringMigration, func2: AnyDuringMigration,\n errorPrefix: string): boolean {\n if (func1 && func2) {\n if (typeof func1 !== 'function' || typeof func2 !== 'function') {\n throw Error(errorPrefix + ' must be a function');\n }\n return true;\n } else if (!func1 && !func2) {\n return false;\n }\n throw Error(errorPrefix + 'Must have both or neither functions');\n}\n\n/**\n * Checks that the given object required mutator properties.\n *\n * @param errorPrefix The string to prepend to any error message.\n * @param object The object to inspect.\n */\nfunction checkHasMutatorProperties(\n errorPrefix: string, object: AnyDuringMigration) {\n const hasXmlHooks = checkXmlHooks(object, errorPrefix);\n const hasJsonHooks = checkJsonHooks(object, errorPrefix);\n if (!hasXmlHooks && !hasJsonHooks) {\n throw Error(\n errorPrefix +\n 'Mutations must contain either XML hooks, or JSON hooks, or both');\n }\n // A block with a mutator isn't required to have a mutation dialog, but\n // it should still have both or neither of compose and decompose.\n checkMutatorDialog(object, errorPrefix);\n}\n\n/**\n * Get a list of values of mutator properties on the given block.\n *\n * @param block The block to inspect.\n * @returns A list with all of the defined properties, which should be\n * functions, but may be anything other than undefined.\n */\nfunction getMutatorProperties(block: Block): AnyDuringMigration[] {\n const result = [];\n // List each function explicitly by reference to allow for renaming\n // during compilation.\n if (block.domToMutation !== undefined) {\n result.push(block.domToMutation);\n }\n if (block.mutationToDom !== undefined) {\n result.push(block.mutationToDom);\n }\n if (block.saveExtraState !== undefined) {\n result.push(block.saveExtraState);\n }\n if (block.loadExtraState !== undefined) {\n result.push(block.loadExtraState);\n }\n if (block.compose !== undefined) {\n result.push(block.compose);\n }\n if (block.decompose !== undefined) {\n result.push(block.decompose);\n }\n return result;\n}\n\n/**\n * Check that the current mutator properties match a list of old mutator\n * properties. This should be called after applying a non-mutator extension,\n * to verify that the extension didn't change properties it shouldn't.\n *\n * @param oldProperties The old values to compare to.\n * @param block The block to inspect for new values.\n * @returns True if the property lists match.\n */\nfunction mutatorPropertiesMatch(\n oldProperties: AnyDuringMigration[], block: Block): boolean {\n const newProperties = getMutatorProperties(block);\n if (newProperties.length !== oldProperties.length) {\n return false;\n }\n for (let i = 0; i < newProperties.length; i++) {\n if (oldProperties[i] !== newProperties[i]) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Calls a function after the page has loaded, possibly immediately.\n *\n * @param fn Function to run.\n * @throws Error Will throw if no global document can be found (e.g., Node.js).\n * @internal\n */\nexport function runAfterPageLoad(fn: () => void) {\n if (typeof document !== 'object') {\n throw Error('runAfterPageLoad() requires browser document.');\n }\n if (document.readyState === 'complete') {\n fn(); // Page has already loaded. Call immediately.\n } else {\n // Poll readyState.\n const readyStateCheckInterval = setInterval(function() {\n if (document.readyState === 'complete') {\n clearInterval(readyStateCheckInterval);\n fn();\n }\n }, 10);\n }\n}\n\n/**\n * Builds an extension function that will map a dropdown value to a tooltip\n * string.\n *\n * This method includes multiple checks to ensure tooltips, dropdown options,\n * and message references are aligned. This aims to catch errors as early as\n * possible, without requiring developers to manually test tooltips under each\n * option. After the page is loaded, each tooltip text string will be checked\n * for matching message keys in the internationalized string table. Deferring\n * this until the page is loaded decouples loading dependencies. Later, upon\n * loading the first block of any given type, the extension will validate every\n * dropdown option has a matching tooltip in the lookupTable. Errors are\n * reported as warnings in the console, and are never fatal.\n *\n * @param dropdownName The name of the field whose value is the key to the\n * lookup table.\n * @param lookupTable The table of field values to tooltip text.\n * @returns The extension function.\n * @alias Blockly.Extensions.buildTooltipForDropdown\n */\nexport function buildTooltipForDropdown(\n dropdownName: string, lookupTable: {[key: string]: string}): Function {\n // List of block types already validated, to minimize duplicate warnings.\n const blockTypesChecked: AnyDuringMigration[] = [];\n\n // Check the tooltip string messages for invalid references.\n // Wait for load, in case Blockly.Msg is not yet populated.\n // runAfterPageLoad() does not run in a Node.js environment due to lack\n // of document object, in which case skip the validation.\n if (typeof document === 'object') { // Relies on document.readyState\n runAfterPageLoad(function() {\n for (const key in lookupTable) {\n // Will print warnings if reference is missing.\n parsing.checkMessageReferences(lookupTable[key]);\n }\n });\n }\n\n /** The actual extension. */\n function extensionFn(this: Block) {\n if (this.type && blockTypesChecked.indexOf(this.type) === -1) {\n checkDropdownOptionsInTable(this, dropdownName, lookupTable);\n blockTypesChecked.push(this.type);\n }\n\n this.setTooltip(function(this: Block) {\n const value = String(this.getFieldValue(dropdownName));\n let tooltip = lookupTable[value];\n if (tooltip === null) {\n if (blockTypesChecked.indexOf(this.type) === -1) {\n // Warn for missing values on generated tooltips.\n let warning = 'No tooltip mapping for value ' + value + ' of field ' +\n dropdownName;\n if (this.type !== null) {\n warning += ' of block type ' + this.type;\n }\n console.warn(warning + '.');\n }\n } else {\n tooltip = parsing.replaceMessageReferences(tooltip);\n }\n return tooltip;\n }.bind(this));\n }\n return extensionFn;\n}\n\n/**\n * Checks all options keys are present in the provided string lookup table.\n * Emits console warnings when they are not.\n *\n * @param block The block containing the dropdown\n * @param dropdownName The name of the dropdown\n * @param lookupTable The string lookup table\n */\nfunction checkDropdownOptionsInTable(\n block: Block, dropdownName: string, lookupTable: {[key: string]: string}) {\n // Validate all dropdown options have values.\n const dropdown = block.getField(dropdownName);\n if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) {\n const options = dropdown.getOptions();\n for (let i = 0; i < options.length; i++) {\n const optionKey = options[i][1]; // label, then value\n if (lookupTable[optionKey] === null) {\n console.warn(\n 'No tooltip mapping for value ' + optionKey + ' of field ' +\n dropdownName + ' of block type ' + block.type);\n }\n }\n }\n}\n\n/**\n * Builds an extension function that will install a dynamic tooltip. The\n * tooltip message should include the string '%1' and that string will be\n * replaced with the text of the named field.\n *\n * @param msgTemplate The template form to of the message text, with %1\n * placeholder.\n * @param fieldName The field with the replacement text.\n * @returns The extension function.\n * @alias Blockly.Extensions.buildTooltipWithFieldText\n */\nexport function buildTooltipWithFieldText(\n msgTemplate: string, fieldName: string): Function {\n // Check the tooltip string messages for invalid references.\n // Wait for load, in case Blockly.Msg is not yet populated.\n // runAfterPageLoad() does not run in a Node.js environment due to lack\n // of document object, in which case skip the validation.\n if (typeof document === 'object') { // Relies on document.readyState\n runAfterPageLoad(function() {\n // Will print warnings if reference is missing.\n parsing.checkMessageReferences(msgTemplate);\n });\n }\n\n /** The actual extension. */\n function extensionFn(this: Block) {\n this.setTooltip(function(this: Block) {\n const field = this.getField(fieldName);\n return parsing.replaceMessageReferences(msgTemplate)\n .replace('%1', field ? field.getText() : '');\n }.bind(this));\n }\n return extensionFn;\n}\n\n/**\n * Configures the tooltip to mimic the parent block when connected. Otherwise,\n * uses the tooltip text at the time this extension is initialized. This takes\n * advantage of the fact that all other values from JSON are initialized before\n * extensions.\n */\nfunction extensionParentTooltip(this: Block) {\n const tooltipWhenNotConnected = this.tooltip;\n this.setTooltip(function(this: Block) {\n const parent = this.getParent();\n return parent && parent.getInputsInline() && parent.tooltip ||\n tooltipWhenNotConnected;\n }.bind(this));\n}\nregister('parent_tooltip_when_inline', extensionParentTooltip);\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/** @namespace Blockly.utils.array */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.array');\n\n\n/**\n * Removes the first occurrence of a particular value from an array.\n *\n * @param arr Array from which to remove value.\n * @param value Value to remove.\n * @returns True if an element was removed.\n * @alias Blockly.array.removeElem\n * @internal\n */\nexport function removeElem(arr: Array, value: T): boolean {\n const i = arr.indexOf(value);\n if (i === -1) {\n return false;\n }\n arr.splice(i, 1);\n return true;\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Methods for creating parts of SVG path strings. See\n *\n * @namespace Blockly.utils.svgPaths\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils.svgPaths');\n\n\n/**\n * Create a string representing the given x, y pair. It does not matter whether\n * the coordinate is relative or absolute. The result has leading\n * and trailing spaces, and separates the x and y coordinates with a comma but\n * no space.\n *\n * @param x The x coordinate.\n * @param y The y coordinate.\n * @returns A string of the format ' x,y '\n * @alias Blockly.utils.svgPaths.point\n */\nexport function point(x: number, y: number): string {\n return ' ' + x + ',' + y + ' ';\n}\n\n/**\n * Draw a cubic or quadratic curve. See\n * developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#Cubic_B%C3%A9zier_Curve\n * These coordinates are unitless and hence in the user coordinate system.\n *\n * @param command The command to use.\n * Should be one of: c, C, s, S, q, Q.\n * @param points An array containing all of the points to pass to the curve\n * command, in order. The points are represented as strings of the format '\n * x, y '.\n * @returns A string defining one or more Bezier curves. See the MDN\n * documentation for exact format.\n * @alias Blockly.utils.svgPaths.curve\n */\nexport function curve(command: string, points: string[]): string {\n return ' ' + command + points.join('');\n}\n\n/**\n * Move the cursor to the given position without drawing a line.\n * The coordinates are absolute.\n * These coordinates are unitless and hence in the user coordinate system.\n * See developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Line_commands\n *\n * @param x The absolute x coordinate.\n * @param y The absolute y coordinate.\n * @returns A string of the format ' M x,y '\n * @alias Blockly.utils.svgPaths.moveTo\n */\nexport function moveTo(x: number, y: number): string {\n return ' M ' + x + ',' + y + ' ';\n}\n\n/**\n * Move the cursor to the given position without drawing a line.\n * Coordinates are relative.\n * These coordinates are unitless and hence in the user coordinate system.\n * See developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Line_commands\n *\n * @param dx The relative x coordinate.\n * @param dy The relative y coordinate.\n * @returns A string of the format ' m dx,dy '\n * @alias Blockly.utils.svgPaths.moveBy\n */\nexport function moveBy(dx: number, dy: number): string {\n return ' m ' + dx + ',' + dy + ' ';\n}\n\n/**\n * Draw a line from the current point to the end point, which is the current\n * point shifted by dx along the x-axis and dy along the y-axis.\n * These coordinates are unitless and hence in the user coordinate system.\n * See developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Line_commands\n *\n * @param dx The relative x coordinate.\n * @param dy The relative y coordinate.\n * @returns A string of the format ' l dx,dy '\n * @alias Blockly.utils.svgPaths.lineTo\n */\nexport function lineTo(dx: number, dy: number): string {\n return ' l ' + dx + ',' + dy + ' ';\n}\n\n/**\n * Draw multiple lines connecting all of the given points in order. This is\n * equivalent to a series of 'l' commands.\n * These coordinates are unitless and hence in the user coordinate system.\n * See developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Line_commands\n *\n * @param points An array containing all of the points to draw lines to, in\n * order. The points are represented as strings of the format ' dx,dy '.\n * @returns A string of the format ' l (dx,dy)+ '\n * @alias Blockly.utils.svgPaths.line\n */\nexport function line(points: string[]): string {\n return ' l' + points.join('');\n}\n\n/**\n * Draw a horizontal or vertical line.\n * The first argument specifies the direction and whether the given position is\n * relative or absolute.\n * These coordinates are unitless and hence in the user coordinate system.\n * See developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#LineTo_path_commands\n *\n * @param command The command to prepend to the coordinate. This should be one\n * of: V, v, H, h.\n * @param val The coordinate to pass to the command. It may be absolute or\n * relative.\n * @returns A string of the format ' command val '\n * @alias Blockly.utils.svgPaths.lineOnAxis\n */\nexport function lineOnAxis(command: string, val: number): string {\n return ' ' + command + ' ' + val + ' ';\n}\n\n/**\n * Draw an elliptical arc curve.\n * These coordinates are unitless and hence in the user coordinate system.\n * See developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#Elliptical_Arc_Curve\n *\n * @param command The command string. Either 'a' or 'A'.\n * @param flags The flag string. See the MDN documentation for a description\n * and examples.\n * @param radius The radius of the arc to draw.\n * @param point The point to move the cursor to after drawing the arc, specified\n * either in absolute or relative coordinates depending on the command.\n * @returns A string of the format 'command radius radius flags point'\n * @alias Blockly.utils.svgPaths.arc\n */\nexport function arc(\n command: string, flags: string, radius: number, point: string): string {\n return command + ' ' + radius + ' ' + radius + ' ' + flags + point;\n}\n","/**\n * @license\n * Copyright 2012 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility methods.\n *\n * @namespace Blockly.utils\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.utils');\n\nimport type {Block} from './block.js';\nimport * as browserEvents from './browser_events.js';\nimport * as common from './common.js';\nimport * as extensions from './extensions.js';\nimport * as aria from './utils/aria.js';\nimport * as arrayUtils from './utils/array.js';\nimport * as colour from './utils/colour.js';\nimport {Coordinate} from './utils/coordinate.js';\nimport * as deprecation from './utils/deprecation.js';\nimport * as dom from './utils/dom.js';\nimport * as idGenerator from './utils/idgenerator.js';\nimport {KeyCodes} from './utils/keycodes.js';\nimport * as math from './utils/math.js';\nimport type {Metrics} from './utils/metrics.js';\nimport * as object from './utils/object.js';\nimport * as parsing from './utils/parsing.js';\nimport {Rect} from './utils/rect.js';\nimport {Size} from './utils/size.js';\nimport * as stringUtils from './utils/string.js';\nimport * as style from './utils/style.js';\nimport {Svg} from './utils/svg.js';\nimport * as svgMath from './utils/svg_math.js';\nimport * as svgPaths from './utils/svg_paths.js';\nimport * as toolbox from './utils/toolbox.js';\nimport * as userAgent from './utils/useragent.js';\nimport * as xml from './utils/xml.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\nexport {\n aria,\n arrayUtils as array,\n browserEvents,\n colour,\n Coordinate,\n deprecation,\n dom,\n extensions,\n idGenerator,\n KeyCodes,\n math,\n Metrics,\n object,\n parsing,\n Rect,\n Size,\n stringUtils as string,\n style,\n Svg,\n svgMath,\n svgPaths,\n toolbox,\n userAgent,\n xml,\n};\n\n/**\n * Return the coordinates of the top-left corner of this element relative to\n * its parent. Only for SVG elements and children (e.g. rect, g, path).\n *\n * @param element SVG element to find the coordinates of.\n * @returns Object with .x and .y properties.\n * @deprecated Use **Blockly.utils.svgMath.getRelativeXY** instead.\n * @alias Blockly.utils.getRelativeXY\n */\nexport function getRelativeXY(element: Element): Coordinate {\n deprecation.warn(\n 'Blockly.utils.getRelativeXY', 'December 2021', 'December 2022',\n 'Blockly.utils.svgMath.getRelativeXY');\n return svgMath.getRelativeXY(element);\n}\n\n/**\n * Return the coordinates of the top-left corner of this element relative to\n * the div Blockly was injected into.\n *\n * @param element SVG element to find the coordinates of. If this is not a child\n * of the div Blockly was injected into, the behaviour is undefined.\n * @returns Object with .x and .y properties.\n * @deprecated Use **Blockly.utils.svgMath.getInjectionDivXY** instead.\n * @alias Blockly.utils.getInjectionDivXY_\n */\nfunction getInjectionDivXY(element: Element): Coordinate {\n deprecation.warn(\n 'Blockly.utils.getInjectionDivXY_', 'December 2021', 'December 2022',\n 'Blockly.utils.svgMath.getInjectionDivXY');\n return svgMath.getInjectionDivXY(element);\n}\nexport const getInjectionDivXY_ = getInjectionDivXY;\n\n/**\n * Parse a string with any number of interpolation tokens (%1, %2, ...).\n * It will also replace string table references (e.g., %{bky_my_msg} and\n * %{BKY_MY_MSG} will both be replaced with the value in\n * Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped\n * (e.g., '%%').\n *\n * @param message Text which might contain string table references and\n * interpolation tokens.\n * @returns Array of strings and numbers.\n * @deprecated Use **Blockly.utils.parsing.tokenizeInterpolation** instead.\n * @alias Blockly.utils.tokenizeInterpolation\n */\nexport function tokenizeInterpolation(message: string): Array {\n deprecation.warn(\n 'Blockly.utils.tokenizeInterpolation', 'December 2021', 'December 2022',\n 'Blockly.utils.parsing.tokenizeInterpolation');\n return parsing.tokenizeInterpolation(message);\n}\n\n/**\n * Replaces string table references in a message, if the message is a string.\n * For example, \"%{bky_my_msg}\" and \"%{BKY_MY_MSG}\" will both be replaced with\n * the value in Msg['MY_MSG'].\n *\n * @param message Message, which may be a string that contains string table\n * references.\n * @returns String with message references replaced.\n * @deprecated Use **Blockly.utils.parsing.replaceMessageReferences** instead.\n * @alias Blockly.utils.replaceMessageReferences\n */\nexport function replaceMessageReferences(message: string|any): string {\n deprecation.warn(\n 'Blockly.utils.replaceMessageReferences', 'December 2021',\n 'December 2022', 'Blockly.utils.parsing.replaceMessageReferences');\n return parsing.replaceMessageReferences(message);\n}\n\n/**\n * Validates that any %{MSG_KEY} references in the message refer to keys of\n * the Msg string table.\n *\n * @param message Text which might contain string table references.\n * @returns True if all message references have matching values.\n * Otherwise, false.\n * @deprecated Use **Blockly.utils.parsing.checkMessageReferences** instead.\n * @alias Blockly.utils.checkMessageReferences\n */\nexport function checkMessageReferences(message: string): boolean {\n deprecation.warn(\n 'Blockly.utils.checkMessageReferences', 'December 2021', 'December 2022',\n 'Blockly.utils.parsing.checkMessageReferences');\n return parsing.checkMessageReferences(message);\n}\n\n/**\n * Check if 3D transforms are supported by adding an element\n * and attempting to set the property.\n *\n * @returns True if 3D transforms are supported.\n * @deprecated Use **Blockly.utils.svgMath.is3dSupported** instead.\n * @alias Blockly.utils.is3dSupported\n */\nexport function is3dSupported(): boolean {\n deprecation.warn(\n 'Blockly.utils.is3dSupported', 'December 2021', 'December 2022',\n 'Blockly.utils.svgMath.is3dSupported');\n return svgMath.is3dSupported();\n}\n\n/**\n * Get the position of the current viewport in window coordinates. This takes\n * scroll into account.\n *\n * @returns An object containing window width, height, and scroll position in\n * window coordinates.\n * @alias Blockly.utils.getViewportBBox\n * @deprecated Use **Blockly.utils.svgMath.getViewportBBox** instead.\n * @internal\n */\nexport function getViewportBBox(): Rect {\n deprecation.warn(\n 'Blockly.utils.getViewportBBox', 'December 2021', 'December 2022',\n 'Blockly.utils.svgMath.getViewportBBox');\n return svgMath.getViewportBBox();\n}\n\n/**\n * Removes the first occurrence of a particular value from an array.\n *\n * @param arr Array from which to remove value.\n * @param value Value to remove.\n * @returns True if an element was removed.\n * @alias Blockly.utils.arrayRemove\n * @deprecated Use **Blockly.array.removeElem** instead.\n * @internal\n */\nexport function arrayRemove(arr: Array, value: T): boolean {\n deprecation.warn(\n 'Blockly.utils.arrayRemove', 'December 2021', 'December 2022',\n 'Blockly.array.removeElem');\n return arrayUtils.removeElem(arr, value);\n}\n\n/**\n * Gets the document scroll distance as a coordinate object.\n * Copied from Closure's goog.dom.getDocumentScroll.\n *\n * @returns Object with values 'x' and 'y'.\n * @deprecated Use **Blockly.utils.svgMath.getDocumentScroll** instead.\n * @alias Blockly.utils.getDocumentScroll\n */\nexport function getDocumentScroll(): Coordinate {\n deprecation.warn(\n 'Blockly.utils.getDocumentScroll', 'December 2021', 'December 2022',\n 'Blockly.utils.svgMath.getDocumentScroll');\n return svgMath.getDocumentScroll();\n}\n\n/**\n * Get a map of all the block's descendants mapping their type to the number of\n * children with that type.\n *\n * @param block The block to map.\n * @param opt_stripFollowing Optionally ignore all following statements (blocks\n * that are not inside a value or statement input of the block).\n * @returns Map of types to type counts for descendants of the bock.\n * @deprecated Use **Blockly.common.getBlockTypeCounts** instead.\n * @alias Blockly.utils.getBlockTypeCounts\n */\nexport function getBlockTypeCounts(\n block: Block, opt_stripFollowing?: boolean): {[key: string]: number} {\n deprecation.warn(\n 'Blockly.utils.getBlockTypeCounts', 'December 2021', 'December 2022',\n 'Blockly.common.getBlockTypeCounts');\n return common.getBlockTypeCounts(block, opt_stripFollowing);\n}\n\n/**\n * Converts screen coordinates to workspace coordinates.\n *\n * @param ws The workspace to find the coordinates on.\n * @param screenCoordinates The screen coordinates to be converted to workspace\n * coordinates\n * @deprecated Use **Blockly.utils.svgMath.screenToWsCoordinates** instead.\n * @returns The workspace coordinates.\n */\nexport function screenToWsCoordinates(\n ws: WorkspaceSvg, screenCoordinates: Coordinate): Coordinate {\n deprecation.warn(\n 'Blockly.utils.screenToWsCoordinates', 'December 2021', 'December 2022',\n 'Blockly.utils.svgMath.screenToWsCoordinates');\n return svgMath.screenToWsCoordinates(ws, screenCoordinates);\n}\n\n/**\n * Parse a block colour from a number or string, as provided in a block\n * definition.\n *\n * @param colour HSV hue value (0 to 360), #RRGGBB string, or a message\n * reference string pointing to one of those two values.\n * @returns An object containing the colour as a #RRGGBB string, and the hue if\n * the input was an HSV hue value.\n * @throws {Error} If the colour cannot be parsed.\n * @deprecated Use **Blockly.utils.parsing.parseBlockColour** instead.\n * @alias Blockly.utils.parseBlockColour\n */\nexport function parseBlockColour(colour: number|\n string): {hue: number|null, hex: string} {\n deprecation.warn(\n 'Blockly.utils.parseBlockColour', 'December 2021', 'December 2022',\n 'Blockly.utils.parsing.parseBlockColour');\n return parsing.parseBlockColour(colour);\n}\n\n/**\n * Calls a function after the page has loaded, possibly immediately.\n *\n * @param fn Function to run.\n * @throws Error Will throw if no global document can be found (e.g., Node.js).\n * @deprecated No longer provided by Blockly.\n * @alias Blockly.utils.runAfterPageLoad\n */\nexport function runAfterPageLoad(fn: () => void) {\n deprecation.warn(\n 'Blockly.utils.runAfterPageLoad', 'December 2021', 'December 2022');\n extensions.runAfterPageLoad(fn);\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Contains functions registering serializers (eg blocks, variables, plugins,\n * etc).\n *\n * @namespace Blockly.serialization.registry\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.serialization.registry');\n\n// eslint-disable-next-line no-unused-vars\nimport type {ISerializer} from '../interfaces/i_serializer.js';\nimport * as registry from '../registry.js';\n\n\n/**\n * Registers the given serializer so that it can be used for serialization and\n * deserialization.\n *\n * @param name The name of the serializer to register.\n * @param serializer The serializer to register.\n * @alias Blockly.serialization.registry.register\n */\nexport function register(name: string, serializer: ISerializer) {\n registry.register(registry.Type.SERIALIZER, name, serializer);\n}\n\n/**\n * Unregisters the serializer associated with the given name.\n *\n * @param name The name of the serializer to unregister.\n * @alias Blockly.serialization.registry.unregister\n */\nexport function unregister(name: string) {\n registry.unregister(registry.Type.SERIALIZER, name);\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Handles serializing blocks to plain JavaScript objects only containing state.\n *\n * @namespace Blockly.serialization.blocks\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.serialization.blocks');\n\nimport type {Block} from '../block.js';\nimport type {BlockSvg} from '../block_svg.js';\nimport type {Connection} from '../connection.js';\nimport * as eventUtils from '../events/utils.js';\nimport {inputTypes} from '../input_types.js';\nimport type {ISerializer} from '../interfaces/i_serializer.js';\nimport {Size} from '../utils/size.js';\nimport type {Workspace} from '../workspace.js';\nimport * as Xml from '../xml.js';\n\nimport {BadConnectionCheck, MissingBlockType, MissingConnection, RealChildOfShadow} from './exceptions.js';\nimport * as priorities from './priorities.js';\nimport * as serializationRegistry from './registry.js';\n\n\n// TODO(#5160): Remove this once lint is fixed.\n/* eslint-disable no-use-before-define */\n\n/**\n * Represents the state of a connection.\n *\n * @alias Blockly.serialization.blocks.ConnectionState\n */\nexport interface ConnectionState {\n shadow: State|undefined;\n block: State|undefined;\n}\n\n/**\n * Represents the state of a given block.\n *\n * @alias Blockly.serialization.blocks.State\n */\nexport interface State {\n type: string;\n id?: string;\n x?: number;\n y?: number;\n collapsed?: boolean;\n enabled?: boolean;\n inline?: boolean;\n data?: string;\n extraState?: AnyDuringMigration;\n icons?: {[key: string]: AnyDuringMigration};\n fields?: {[key: string]: AnyDuringMigration};\n inputs?: {[key: string]: ConnectionState};\n next?: ConnectionState;\n}\n\n/**\n * Returns the state of the given block as a plain JavaScript object.\n *\n * @param block The block to serialize.\n * @param param1 addCoordinates: If true, the coordinates of the block are added\n * to the serialized state. False by default. addinputBlocks: If true,\n * children of the block which are connected to inputs will be serialized.\n * True by default. addNextBlocks: If true, children of the block which are\n * connected to the block's next connection (if it exists) will be\n * serialized. True by default. doFullSerialization: If true, fields that\n * normally just save a reference to some external state (eg variables) will\n * instead serialize all of the info about that state. This supports\n * deserializing the block into a workspace where that state doesn't yet\n * exist. True by default.\n * @returns The serialized state of the block, or null if the block could not be\n * serialied (eg it was an insertion marker).\n * @alias Blockly.serialization.blocks.save\n */\nexport function save(block: Block, {\n addCoordinates = false,\n addInputBlocks = true,\n addNextBlocks = true,\n doFullSerialization = true,\n}: {\n addCoordinates?: boolean,\n addInputBlocks?: boolean,\n addNextBlocks?: boolean,\n doFullSerialization?: boolean\n} = {}): State|null {\n if (block.isInsertionMarker()) {\n return null;\n }\n\n const state = {\n 'type': block.type,\n 'id': block.id,\n };\n\n if (addCoordinates) {\n // AnyDuringMigration because: Argument of type '{ type: string; id:\n // string; }' is not assignable to parameter of type 'State'.\n saveCoords(block, state as AnyDuringMigration);\n }\n // AnyDuringMigration because: Argument of type '{ type: string; id: string;\n // }' is not assignable to parameter of type 'State'.\n saveAttributes(block, state as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type '{ type: string; id: string;\n // }' is not assignable to parameter of type 'State'.\n saveExtraState(block, state as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type '{ type: string; id: string;\n // }' is not assignable to parameter of type 'State'.\n saveIcons(block, state as AnyDuringMigration);\n // AnyDuringMigration because: Argument of type '{ type: string; id: string;\n // }' is not assignable to parameter of type 'State'.\n saveFields(block, state as AnyDuringMigration, doFullSerialization);\n if (addInputBlocks) {\n // AnyDuringMigration because: Argument of type '{ type: string; id:\n // string; }' is not assignable to parameter of type 'State'.\n saveInputBlocks(block, state as AnyDuringMigration, doFullSerialization);\n }\n if (addNextBlocks) {\n // AnyDuringMigration because: Argument of type '{ type: string; id:\n // string; }' is not assignable to parameter of type 'State'.\n saveNextBlocks(block, state as AnyDuringMigration, doFullSerialization);\n }\n\n // AnyDuringMigration because: Type '{ type: string; id: string; }' is not\n // assignable to type 'State'.\n return state as AnyDuringMigration;\n}\n\n/**\n * Adds attributes to the given state object based on the state of the block.\n * Eg collapsed, disabled, inline, etc.\n *\n * @param block The block to base the attributes on.\n * @param state The state object to append to.\n */\nfunction saveAttributes(block: Block, state: State) {\n if (block.isCollapsed()) {\n state['collapsed'] = true;\n }\n if (!block.isEnabled()) {\n state['enabled'] = false;\n }\n if (block.inputsInline !== undefined &&\n block.inputsInline !== block.inputsInlineDefault) {\n state['inline'] = block.inputsInline;\n }\n // Data is a nullable string, so we don't need to worry about falsy values.\n if (block.data) {\n state['data'] = block.data;\n }\n}\n\n/**\n * Adds the coordinates of the given block to the given state object.\n *\n * @param block The block to base the coordinates on.\n * @param state The state object to append to.\n */\nfunction saveCoords(block: Block, state: State) {\n const workspace = block.workspace;\n const xy = block.getRelativeToSurfaceXY();\n state['x'] = Math.round(workspace.RTL ? workspace.getWidth() - xy.x : xy.x);\n state['y'] = Math.round(xy.y);\n}\n/**\n * Adds any extra state the block may provide to the given state object.\n *\n * @param block The block to serialize the extra state of.\n * @param state The state object to append to.\n */\nfunction saveExtraState(block: Block, state: State) {\n if (block.saveExtraState) {\n const extraState = block.saveExtraState();\n if (extraState !== null) {\n state['extraState'] = extraState;\n }\n } else if (block.mutationToDom) {\n const extraState = block.mutationToDom();\n if (extraState !== null) {\n state['extraState'] =\n Xml.domToText(extraState)\n .replace(\n ' xmlns=\"https://developers.google.com/blockly/xml\"', '');\n }\n }\n}\n\n/**\n * Adds the state of all of the icons on the block to the given state object.\n *\n * @param block The block to serialize the icon state of.\n * @param state The state object to append to.\n */\nfunction saveIcons(block: Block, state: State) {\n // TODO(#2105): Remove this logic and put it in the icon.\n if (block.getCommentText()) {\n state['icons'] = {\n 'comment': {\n 'text': block.getCommentText(),\n 'pinned': block.commentModel.pinned,\n 'height': Math.round(block.commentModel.size.height),\n 'width': Math.round(block.commentModel.size.width),\n },\n };\n }\n}\n\n/**\n * Adds the state of all of the fields on the block to the given state object.\n *\n * @param block The block to serialize the field state of.\n * @param state The state object to append to.\n * @param doFullSerialization Whether or not to serialize the full state of the\n * field (rather than possibly saving a reference to some state).\n */\nfunction saveFields(block: Block, state: State, doFullSerialization: boolean) {\n const fields = Object.create(null);\n for (let i = 0; i < block.inputList.length; i++) {\n const input = block.inputList[i];\n for (let j = 0; j < input.fieldRow.length; j++) {\n const field = input.fieldRow[j];\n if (field.isSerializable()) {\n fields[field.name!] = field.saveState(doFullSerialization);\n }\n }\n }\n if (Object.keys(fields).length) {\n state['fields'] = fields;\n }\n}\n\n/**\n * Adds the state of all of the child blocks of the given block (which are\n * connected to inputs) to the given state object.\n *\n * @param block The block to serialize the input blocks of.\n * @param state The state object to append to.\n * @param doFullSerialization Whether or not to do full serialization.\n */\nfunction saveInputBlocks(\n block: Block, state: State, doFullSerialization: boolean) {\n const inputs = Object.create(null);\n for (let i = 0; i < block.inputList.length; i++) {\n const input = block.inputList[i];\n if (input.type === inputTypes.DUMMY) {\n continue;\n }\n const connectionState =\n saveConnection(input.connection as Connection, doFullSerialization);\n if (connectionState) {\n inputs[input.name] = connectionState;\n }\n }\n\n if (Object.keys(inputs).length) {\n state['inputs'] = inputs;\n }\n}\n\n/**\n * Adds the state of all of the next blocks of the given block to the given\n * state object.\n *\n * @param block The block to serialize the next blocks of.\n * @param state The state object to append to.\n * @param doFullSerialization Whether or not to do full serialization.\n */\nfunction saveNextBlocks(\n block: Block, state: State, doFullSerialization: boolean) {\n if (!block.nextConnection) {\n return;\n }\n const connectionState =\n saveConnection(block.nextConnection, doFullSerialization);\n if (connectionState) {\n state['next'] = connectionState;\n }\n}\n\n/**\n * Returns the state of the given connection (ie the state of any connected\n * shadow or real blocks).\n *\n * @param connection The connection to serialize the connected blocks of.\n * @returns An object containing the state of any connected shadow block, or any\n * connected real block.\n * @param doFullSerialization Whether or not to do full serialization.\n */\nfunction saveConnection(connection: Connection, doFullSerialization: boolean):\n ConnectionState|null {\n const shadow = connection.getShadowState(true);\n const child = connection.targetBlock();\n if (!shadow && !child) {\n return null;\n }\n const state = Object.create(null);\n if (shadow) {\n state['shadow'] = shadow;\n }\n if (child && !child.isShadow()) {\n state['block'] = save(child, {doFullSerialization});\n }\n return state;\n}\n\n/**\n * Loads the block represented by the given state into the given workspace.\n *\n * @param state The state of a block to deserialize into the workspace.\n * @param workspace The workspace to add the block to.\n * @param param1 recordUndo: If true, events triggered by this function will be\n * undo-able by the user. False by default.\n * @returns The block that was just loaded.\n * @alias Blockly.serialization.blocks.append\n */\nexport function append(\n state: State, workspace: Workspace,\n {recordUndo = false}: {recordUndo?: boolean} = {}): Block {\n return appendInternal(state, workspace, {recordUndo});\n}\n\n/**\n * Loads the block represented by the given state into the given workspace.\n * This is defined internally so that the extra parameters don't clutter our\n * external API.\n * But it is exported so that other places within Blockly can call it directly\n * with the extra parameters.\n *\n * @param state The state of a block to deserialize into the workspace.\n * @param workspace The workspace to add the block to.\n * @param param1 parentConnection: If provided, the system will attempt to\n * connect the block to this connection after it is created. Undefined by\n * default. isShadow: If true, the block will be set to a shadow block after\n * it is created. False by default. recordUndo: If true, events triggered by\n * this function will be undo-able by the user. False by default.\n * @returns The block that was just appended.\n * @alias Blockly.serialization.blocks.appendInternal\n * @internal\n */\nexport function appendInternal(\n state: State, workspace: Workspace,\n {parentConnection = undefined, isShadow = false, recordUndo = false}: {\n parentConnection?: Connection,\n isShadow?: boolean,\n recordUndo?: boolean\n } = {}): Block {\n const prevRecordUndo = eventUtils.getRecordUndo();\n eventUtils.setRecordUndo(recordUndo);\n const existingGroup = eventUtils.getGroup();\n if (!existingGroup) {\n eventUtils.setGroup(true);\n }\n eventUtils.disable();\n\n const block = appendPrivate(state, workspace, {parentConnection, isShadow});\n\n eventUtils.enable();\n eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));\n eventUtils.setGroup(existingGroup);\n eventUtils.setRecordUndo(prevRecordUndo);\n\n // Adding connections to the connection db is expensive. This defers that\n // operation to decrease load time.\n if (workspace.rendered) {\n const blockSvg = block as BlockSvg;\n setTimeout(() => {\n if (!blockSvg.disposed) {\n blockSvg.setConnectionTracking(true);\n }\n }, 1);\n }\n\n return block;\n}\n\n/**\n * Loads the block represented by the given state into the given workspace.\n * This is defined privately so that it can be called recursively without firing\n * eroneous events. Events (and other things we only want to occur on the top\n * block) are handled by appendInternal.\n *\n * @param state The state of a block to deserialize into the workspace.\n * @param workspace The workspace to add the block to.\n * @param param1 parentConnection: If provided, the system will attempt to\n * connect the block to this connection after it is created. Undefined by\n * default. isShadow: The block will be set to a shadow block after it is\n * created. False by default.\n * @returns The block that was just appended.\n */\nfunction appendPrivate(\n state: State, workspace: Workspace,\n {parentConnection = undefined, isShadow = false}:\n {parentConnection?: Connection, isShadow?: boolean} = {}): Block {\n if (!state['type']) {\n throw new MissingBlockType(state);\n }\n\n const block = workspace.newBlock(state['type'], state['id']);\n block.setShadow(isShadow);\n loadCoords(block, state);\n loadAttributes(block, state);\n loadExtraState(block, state);\n tryToConnectParent(parentConnection, block, state);\n loadIcons(block, state);\n loadFields(block, state);\n loadInputBlocks(block, state);\n loadNextBlocks(block, state);\n initBlock(block, workspace.rendered);\n\n return block;\n}\n\n/**\n * Applies any coordinate information available on the state object to the\n * block.\n *\n * @param block The block to set the position of.\n * @param state The state object to reference.\n */\nfunction loadCoords(block: Block, state: State) {\n let x = state['x'] === undefined ? 0 : state['x'];\n const y = state['y'] === undefined ? 0 : state['y'];\n\n const workspace = block.workspace;\n x = workspace.RTL ? workspace.getWidth() - x : x;\n\n block.moveBy(x, y);\n}\n\n/**\n * Applies any attribute information available on the state object to the block.\n *\n * @param block The block to set the attributes of.\n * @param state The state object to reference.\n */\nfunction loadAttributes(block: Block, state: State) {\n if (state['collapsed']) {\n block.setCollapsed(true);\n }\n if (state['enabled'] === false) {\n block.setEnabled(false);\n }\n if (state['inline'] !== undefined) {\n block.setInputsInline(state['inline']);\n }\n if (state['data'] !== undefined) {\n block.data = state['data'];\n }\n}\n\n/**\n * Applies any extra state information available on the state object to the\n * block.\n *\n * @param block The block to set the extra state of.\n * @param state The state object to reference.\n */\nfunction loadExtraState(block: Block, state: State) {\n if (!state['extraState']) {\n return;\n }\n if (block.loadExtraState) {\n block.loadExtraState(state['extraState']);\n } else if (block.domToMutation) {\n block.domToMutation(Xml.textToDom(state['extraState']));\n }\n}\n\n/**\n * Attempts to connect the block to the parent connection, if it exists.\n *\n * @param parentConnection The parent connection to try to connect the block to.\n * @param child The block to try to connect to the parent.\n * @param state The state which defines the given block\n */\nfunction tryToConnectParent(\n parentConnection: Connection|undefined, child: Block, state: State) {\n if (!parentConnection) {\n return;\n }\n\n if (parentConnection.getSourceBlock().isShadow() && !child.isShadow()) {\n throw new RealChildOfShadow(state);\n }\n\n let connected = false;\n let childConnection;\n if (parentConnection.type === inputTypes.VALUE) {\n childConnection = child.outputConnection;\n if (!childConnection) {\n throw new MissingConnection('output', child, state);\n }\n connected = parentConnection.connect(childConnection);\n } else { // Statement type.\n childConnection = child.previousConnection;\n if (!childConnection) {\n throw new MissingConnection('previous', child, state);\n }\n connected = parentConnection.connect(childConnection);\n }\n\n if (!connected) {\n const checker = child.workspace.connectionChecker;\n throw new BadConnectionCheck(\n checker.getErrorMessage(\n checker.canConnectWithReason(\n childConnection, parentConnection, false),\n childConnection, parentConnection),\n parentConnection.type === inputTypes.VALUE ? 'output connection' :\n 'previous connection',\n child, state);\n }\n}\n\n/**\n * Applies icon state to the icons on the block, based on the given state\n * object.\n *\n * @param block The block to set the icon state of.\n * @param state The state object to reference.\n */\nfunction loadIcons(block: Block, state: State) {\n if (!state['icons']) {\n return;\n }\n // TODO(#2105): Remove this logic and put it in the icon.\n const comment = state['icons']['comment'];\n if (comment) {\n block.setCommentText(comment['text']);\n // Load if saved. (Cleaned unnecessary attributes when in the trashcan.)\n if ('pinned' in comment) {\n block.commentModel.pinned = comment['pinned'];\n }\n if ('width' in comment && 'height' in comment) {\n block.commentModel.size = new Size(comment['width'], comment['height']);\n }\n if (comment['pinned'] && block.rendered && !block.isInFlyout) {\n // Give the block a chance to be positioned and rendered before showing.\n const blockSvg = block as BlockSvg;\n setTimeout(() => blockSvg.getCommentIcon()!.setVisible(true), 1);\n }\n }\n}\n\n/**\n * Applies any field information available on the state object to the block.\n *\n * @param block The block to set the field state of.\n * @param state The state object to reference.\n */\nfunction loadFields(block: Block, state: State) {\n if (!state['fields']) {\n return;\n }\n const keys = Object.keys(state['fields']);\n for (let i = 0; i < keys.length; i++) {\n const fieldName = keys[i];\n const fieldState = state['fields'][fieldName];\n const field = block.getField(fieldName);\n if (!field) {\n console.warn(\n `Ignoring non-existant field ${fieldName} in block ${block.type}`);\n continue;\n }\n field.loadState(fieldState);\n }\n}\n\n/**\n * Creates any child blocks (attached to inputs) defined by the given state\n * and attaches them to the given block.\n *\n * @param block The block to attach input blocks to.\n * @param state The state object to reference.\n */\nfunction loadInputBlocks(block: Block, state: State) {\n if (!state['inputs']) {\n return;\n }\n const keys = Object.keys(state['inputs']);\n for (let i = 0; i < keys.length; i++) {\n const inputName = keys[i];\n const input = block.getInput(inputName);\n if (!input || !input.connection) {\n throw new MissingConnection(inputName, block, state);\n }\n loadConnection(input.connection, state['inputs'][inputName]);\n }\n}\n\n/**\n * Creates any next blocks defined by the given state and attaches them to the\n * given block.\n *\n * @param block The block to attach next blocks to.\n * @param state The state object to reference.\n */\nfunction loadNextBlocks(block: Block, state: State) {\n if (!state['next']) {\n return;\n }\n if (!block.nextConnection) {\n throw new MissingConnection('next', block, state);\n }\n loadConnection(block.nextConnection, state['next']);\n}\n/**\n * Applies the state defined by connectionState to the given connection, ie\n * assigns shadows and attaches child blocks.\n *\n * @param connection The connection to deserialize the connected blocks of.\n * @param connectionState The object containing the state of any connected\n * shadow block, or any connected real block.\n */\nfunction loadConnection(\n connection: Connection, connectionState: ConnectionState) {\n if (connectionState['shadow']) {\n connection.setShadowState(connectionState['shadow']);\n }\n if (connectionState['block']) {\n appendPrivate(\n connectionState['block'], connection.getSourceBlock().workspace,\n {parentConnection: connection});\n }\n}\n\n// TODO(#5146): Remove this from the serialization system.\n/**\n * Initializes the give block, eg init the model, inits the svg, renders, etc.\n *\n * @param block The block to initialize.\n * @param rendered Whether the block is a rendered or headless block.\n */\nfunction initBlock(block: Block, rendered: boolean) {\n if (rendered) {\n const blockSvg = block as BlockSvg;\n // Adding connections to the connection db is expensive. This defers that\n // operation to decrease load time.\n blockSvg.setConnectionTracking(false);\n\n blockSvg.initSvg();\n blockSvg.render(false);\n // fixes #6076 JSO deserialization doesn't\n // set .iconXY_ property so here it will be set\n const icons = blockSvg.getIcons();\n for (let i = 0; i < icons.length; i++) {\n icons[i].computeIconLocation();\n }\n } else {\n block.initModel();\n }\n}\n\n// Alias to disambiguate saving within the serializer.\nconst saveBlock = save;\n\n/**\n * Serializer for saving and loading block state.\n *\n * @alias Blockly.serialization.blocks.BlockSerializer\n */\nclass BlockSerializer implements ISerializer {\n priority: number;\n\n /* eslint-disable-next-line require-jsdoc */\n constructor() {\n /** The priority for deserializing blocks. */\n this.priority = priorities.BLOCKS;\n }\n\n /**\n * Serializes the blocks of the given workspace.\n *\n * @param workspace The workspace to save the blocks of.\n * @returns The state of the workspace's blocks, or null if there are no\n * blocks.\n */\n save(workspace: Workspace): {languageVersion: number, blocks: State[]}|null {\n const blockStates = [];\n for (const block of workspace.getTopBlocks(false)) {\n const state =\n saveBlock(block, {addCoordinates: true, doFullSerialization: false});\n if (state) {\n blockStates.push(state);\n }\n }\n if (blockStates.length) {\n return {\n 'languageVersion': 0, // Currently unused.\n 'blocks': blockStates,\n };\n }\n return null;\n }\n\n /**\n * Deserializes the blocks defined by the given state into the given\n * workspace.\n *\n * @param state The state of the blocks to deserialize.\n * @param workspace The workspace to deserialize into.\n */\n load(\n state: {languageVersion: number, blocks: State[]}, workspace: Workspace) {\n const blockStates = state['blocks'];\n for (const state of blockStates) {\n append(state, workspace, {recordUndo: eventUtils.getRecordUndo()});\n }\n }\n\n /**\n * Disposes of any blocks that exist on the workspace.\n *\n * @param workspace The workspace to clear the blocks of.\n */\n clear(workspace: Workspace) {\n // Cannot use workspace.clear() because that also removes variables.\n for (const block of workspace.getTopBlocks(false)) {\n block.dispose(false);\n }\n }\n}\n\nserializationRegistry.register('blocks', new BlockSerializer());\n","/**\n * @license\n * Copyright 2011 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Components for creating connections between blocks.\n *\n * @class\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Connection');\n\nimport type {Block} from './block.js';\nimport {ConnectionType} from './connection_type.js';\nimport type {BlockMove} from './events/events_block_move.js';\nimport * as eventUtils from './events/utils.js';\nimport type {Input} from './input.js';\nimport type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';\nimport type {IConnectionChecker} from './interfaces/i_connection_checker.js';\nimport * as blocks from './serialization/blocks.js';\nimport * as Xml from './xml.js';\n\n\n/**\n * Class for a connection between blocks.\n *\n * @alias Blockly.Connection\n */\nexport class Connection implements IASTNodeLocationWithBlock {\n /** Constants for checking whether two connections are compatible. */\n static CAN_CONNECT = 0;\n static REASON_SELF_CONNECTION = 1;\n static REASON_WRONG_TYPE = 2;\n static REASON_TARGET_NULL = 3;\n static REASON_CHECKS_FAILED = 4;\n static REASON_DIFFERENT_WORKSPACES = 5;\n static REASON_SHADOW_PARENT = 6;\n static REASON_DRAG_CHECKS_FAILED = 7;\n static REASON_PREVIOUS_AND_OUTPUT = 8;\n\n protected sourceBlock_: Block;\n\n /** Connection this connection connects to. Null if not connected. */\n targetConnection: Connection|null = null;\n\n /**\n * Has this connection been disposed of?\n *\n * @internal\n */\n disposed = false;\n\n /** List of compatible value types. Null if all types are compatible. */\n private check_: string[]|null = null;\n\n /** DOM representation of a shadow block, or null if none. */\n private shadowDom_: Element|null = null;\n\n /**\n * Horizontal location of this connection.\n *\n * @internal\n */\n x = 0;\n\n /**\n * Vertical location of this connection.\n *\n * @internal\n */\n y = 0;\n\n private shadowState_: blocks.State|null = null;\n\n /**\n * @param source The block establishing this connection.\n * @param type The type of the connection.\n */\n constructor(source: Block, public type: number) {\n this.sourceBlock_ = source;\n }\n\n /**\n * Connect two connections together. This is the connection on the superior\n * block.\n *\n * @param childConnection Connection on inferior block.\n */\n protected connect_(childConnection: Connection) {\n const INPUT = ConnectionType.INPUT_VALUE;\n const parentBlock = this.getSourceBlock();\n const childBlock = childConnection.getSourceBlock();\n\n // Make sure the childConnection is available.\n if (childConnection.isConnected()) {\n childConnection.disconnect();\n }\n\n // Make sure the parentConnection is available.\n let orphan;\n if (this.isConnected()) {\n const shadowState = this.stashShadowState_();\n const target = this.targetBlock();\n if (target!.isShadow()) {\n target!.dispose(false);\n } else {\n this.disconnect();\n orphan = target;\n }\n this.applyShadowState_(shadowState);\n }\n\n // Connect the new connection to the parent.\n let event;\n if (eventUtils.isEnabled()) {\n event =\n new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock) as BlockMove;\n }\n connectReciprocally(this, childConnection);\n childBlock.setParent(parentBlock);\n if (event) {\n event.recordNew();\n eventUtils.fire(event);\n }\n\n // Deal with the orphan if it exists.\n if (orphan) {\n const orphanConnection = this.type === INPUT ? orphan.outputConnection :\n orphan.previousConnection;\n const connection = Connection.getConnectionForOrphanedConnection(\n childBlock, (orphanConnection));\n if (connection) {\n orphanConnection.connect(connection);\n } else {\n orphanConnection.onFailedConnect(this);\n }\n }\n }\n\n /**\n * Dispose of this connection and deal with connected blocks.\n *\n * @internal\n */\n dispose() {\n // isConnected returns true for shadows and non-shadows.\n if (this.isConnected()) {\n // Destroy the attached shadow block & its children (if it exists).\n this.setShadowStateInternal_();\n\n const targetBlock = this.targetBlock();\n if (targetBlock) {\n // Disconnect the attached normal block.\n targetBlock.unplug();\n }\n }\n\n this.disposed = true;\n }\n\n /**\n * Get the source block for this connection.\n *\n * @returns The source block.\n */\n getSourceBlock(): Block {\n return this.sourceBlock_;\n }\n\n /**\n * Does the connection belong to a superior block (higher in the source\n * stack)?\n *\n * @returns True if connection faces down or right.\n */\n isSuperior(): boolean {\n return this.type === ConnectionType.INPUT_VALUE ||\n this.type === ConnectionType.NEXT_STATEMENT;\n }\n\n /**\n * Is the connection connected?\n *\n * @returns True if connection is connected to another connection.\n */\n isConnected(): boolean {\n return !!this.targetConnection;\n }\n\n /**\n * Get the workspace's connection type checker object.\n *\n * @returns The connection type checker for the source block's workspace.\n * @internal\n */\n getConnectionChecker(): IConnectionChecker {\n return this.sourceBlock_.workspace.connectionChecker;\n }\n\n /**\n * Called when an attempted connection fails. NOP by default (i.e. for\n * headless workspaces).\n *\n * @param _otherConnection Connection that this connection failed to connect\n * to.\n * @internal\n */\n onFailedConnect(_otherConnection: Connection) {}\n // NOP\n\n /**\n * Connect this connection to another connection.\n *\n * @param otherConnection Connection to connect to.\n * @returns Whether the the blocks are now connected or not.\n */\n connect(otherConnection: Connection): boolean {\n if (this.targetConnection === otherConnection) {\n // Already connected together. NOP.\n return true;\n }\n\n const checker = this.getConnectionChecker();\n if (checker.canConnect(this, otherConnection, false)) {\n const eventGroup = eventUtils.getGroup();\n if (!eventGroup) {\n eventUtils.setGroup(true);\n }\n // Determine which block is superior (higher in the source stack).\n if (this.isSuperior()) {\n // Superior block.\n this.connect_(otherConnection);\n } else {\n // Inferior block.\n otherConnection.connect_(this);\n }\n if (!eventGroup) {\n eventUtils.setGroup(false);\n }\n }\n\n return this.isConnected();\n }\n\n /** Disconnect this connection. */\n disconnect() {\n const otherConnection = this.targetConnection;\n if (!otherConnection) {\n throw Error('Source connection not connected.');\n }\n if (otherConnection.targetConnection !== this) {\n throw Error('Target connection not connected to source connection.');\n }\n let parentBlock;\n let childBlock;\n let parentConnection;\n if (this.isSuperior()) {\n // Superior block.\n parentBlock = this.sourceBlock_;\n childBlock = otherConnection.getSourceBlock();\n /* eslint-disable-next-line @typescript-eslint/no-this-alias */\n parentConnection = this;\n } else {\n // Inferior block.\n parentBlock = otherConnection.getSourceBlock();\n childBlock = this.sourceBlock_;\n parentConnection = otherConnection;\n }\n\n const eventGroup = eventUtils.getGroup();\n if (!eventGroup) {\n eventUtils.setGroup(true);\n }\n this.disconnectInternal_(parentBlock, childBlock);\n if (!childBlock.isShadow()) {\n // If we were disconnecting a shadow, no need to spawn a new one.\n parentConnection.respawnShadow_();\n }\n if (!eventGroup) {\n eventUtils.setGroup(false);\n }\n }\n\n /**\n * Disconnect two blocks that are connected by this connection.\n *\n * @param parentBlock The superior block.\n * @param childBlock The inferior block.\n */\n protected disconnectInternal_(parentBlock: Block, childBlock: Block) {\n let event;\n if (eventUtils.isEnabled()) {\n event =\n new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock) as BlockMove;\n }\n const otherConnection = this.targetConnection;\n if (otherConnection) {\n otherConnection.targetConnection = null;\n }\n this.targetConnection = null;\n childBlock.setParent(null);\n if (event) {\n event.recordNew();\n eventUtils.fire(event);\n }\n }\n\n /**\n * Respawn the shadow block if there was one connected to the this connection.\n */\n protected respawnShadow_() {\n // Have to keep respawnShadow_ for backwards compatibility.\n this.createShadowBlock_(true);\n }\n\n /**\n * Returns the block that this connection connects to.\n *\n * @returns The connected block or null if none is connected.\n */\n targetBlock(): Block|null {\n if (this.isConnected()) {\n return this.targetConnection?.getSourceBlock() ?? null;\n }\n return null;\n }\n\n /**\n * Function to be called when this connection's compatible types have changed.\n */\n protected onCheckChanged_() {\n // The new value type may not be compatible with the existing connection.\n if (this.isConnected() &&\n (!this.targetConnection ||\n !this.getConnectionChecker().canConnect(\n this, this.targetConnection, false))) {\n const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;\n child!.unplug();\n }\n }\n\n /**\n * Change a connection's compatibility.\n *\n * @param check Compatible value type or list of value types. Null if all\n * types are compatible.\n * @returns The connection being modified (to allow chaining).\n */\n setCheck(check: string|string[]|null): Connection {\n if (check) {\n if (!Array.isArray(check)) {\n check = [check];\n }\n this.check_ = check;\n this.onCheckChanged_();\n } else {\n this.check_ = null;\n }\n return this;\n }\n\n /**\n * Get a connection's compatibility.\n *\n * @returns List of compatible value types.\n * Null if all types are compatible.\n */\n getCheck(): string[]|null {\n return this.check_;\n }\n\n /**\n * Changes the connection's shadow block.\n *\n * @param shadowDom DOM representation of a block or null.\n */\n setShadowDom(shadowDom: Element|null) {\n this.setShadowStateInternal_({shadowDom});\n }\n\n /**\n * Returns the xml representation of the connection's shadow block.\n *\n * @param returnCurrent If true, and the shadow block is currently attached to\n * this connection, this serializes the state of that block and returns it\n * (so that field values are correct). Otherwise the saved shadowDom is\n * just returned.\n * @returns Shadow DOM representation of a block or null.\n */\n getShadowDom(returnCurrent?: boolean): Element|null {\n return returnCurrent && this.targetBlock()!.isShadow() ?\n Xml.blockToDom((this.targetBlock() as Block)) as Element :\n this.shadowDom_;\n }\n\n /**\n * Changes the connection's shadow block.\n *\n * @param shadowState An state represetation of the block or null.\n */\n setShadowState(shadowState: blocks.State|null) {\n this.setShadowStateInternal_({shadowState});\n }\n\n /**\n * Returns the serialized object representation of the connection's shadow\n * block.\n *\n * @param returnCurrent If true, and the shadow block is currently attached to\n * this connection, this serializes the state of that block and returns it\n * (so that field values are correct). Otherwise the saved state is just\n * returned.\n * @returns Serialized object representation of the block, or null.\n */\n getShadowState(returnCurrent?: boolean): blocks.State|null {\n if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) {\n return blocks.save(this.targetBlock() as Block);\n }\n return this.shadowState_;\n }\n\n /**\n * Find all nearby compatible connections to this connection.\n * Type checking does not apply, since this function is used for bumping.\n *\n * Headless configurations (the default) do not have neighboring connection,\n * and always return an empty list (the default).\n * {@link RenderedConnection#neighbours} overrides this behavior with a list\n * computed from the rendered positioning.\n *\n * @param _maxLimit The maximum radius to another connection.\n * @returns List of connections.\n * @internal\n */\n neighbours(_maxLimit: number): Connection[] {\n return [];\n }\n\n /**\n * Get the parent input of a connection.\n *\n * @returns The input that the connection belongs to or null if no parent\n * exists.\n * @internal\n */\n getParentInput(): Input|null {\n let parentInput = null;\n const inputs = this.sourceBlock_.inputList;\n for (let i = 0; i < inputs.length; i++) {\n if (inputs[i].connection === this) {\n parentInput = inputs[i];\n break;\n }\n }\n return parentInput;\n }\n\n /**\n * This method returns a string describing this Connection in developer terms\n * (English only). Intended to on be used in console logs and errors.\n *\n * @returns The description.\n */\n toString(): string {\n const block = this.sourceBlock_;\n if (!block) {\n return 'Orphan Connection';\n }\n let msg;\n if (block.outputConnection === this) {\n msg = 'Output Connection of ';\n } else if (block.previousConnection === this) {\n msg = 'Previous Connection of ';\n } else if (block.nextConnection === this) {\n msg = 'Next Connection of ';\n } else {\n let parentInput = null;\n for (let i = 0, input; input = block.inputList[i]; i++) {\n if (input.connection === this) {\n parentInput = input;\n break;\n }\n }\n if (parentInput) {\n msg = 'Input \"' + parentInput.name + '\" connection on ';\n } else {\n console.warn('Connection not actually connected to sourceBlock_');\n return 'Orphan Connection';\n }\n }\n return msg + block.toDevString();\n }\n\n /**\n * Returns the state of the shadowDom_ and shadowState_ properties, then\n * temporarily sets those properties to null so no shadow respawns.\n *\n * @returns The state of both the shadowDom_ and shadowState_ properties.\n */\n private stashShadowState_():\n {shadowDom: Element|null, shadowState: blocks.State|null} {\n const shadowDom = this.getShadowDom(true);\n const shadowState = this.getShadowState(true);\n // Set to null so it doesn't respawn.\n this.shadowDom_ = null;\n this.shadowState_ = null;\n return {shadowDom, shadowState};\n }\n\n /**\n * Reapplies the stashed state of the shadowDom_ and shadowState_ properties.\n *\n * @param param0 The state to reapply to the shadowDom_ and shadowState_\n * properties.\n */\n private applyShadowState_({shadowDom, shadowState}: {\n shadowDom: Element|null,\n shadowState: blocks.State|null\n }) {\n this.shadowDom_ = shadowDom;\n this.shadowState_ = shadowState;\n }\n\n /**\n * Sets the state of the shadow of this connection.\n *\n * @param param0 The state to set the shadow of this connection to.\n */\n private setShadowStateInternal_({shadowDom = null, shadowState = null}: {\n shadowDom?: Element|null,\n shadowState?: blocks.State|null\n } = {}) {\n // One or both of these should always be null.\n // If neither is null, the shadowState will get priority.\n this.shadowDom_ = shadowDom;\n this.shadowState_ = shadowState;\n\n const target = this.targetBlock();\n if (!target) {\n this.respawnShadow_();\n if (this.targetBlock() && this.targetBlock()!.isShadow()) {\n this.serializeShadow_(this.targetBlock());\n }\n } else if (target.isShadow()) {\n target.dispose(false);\n this.respawnShadow_();\n if (this.targetBlock() && this.targetBlock()!.isShadow()) {\n this.serializeShadow_(this.targetBlock());\n }\n } else {\n const shadow = this.createShadowBlock_(false);\n this.serializeShadow_(shadow);\n if (shadow) {\n shadow.dispose(false);\n }\n }\n }\n\n /**\n * Creates a shadow block based on the current shadowState_ or shadowDom_.\n * shadowState_ gets priority.\n *\n * @param attemptToConnect Whether to try to connect the shadow block to this\n * connection or not.\n * @returns The shadow block that was created, or null if both the\n * shadowState_ and shadowDom_ are null.\n */\n private createShadowBlock_(attemptToConnect: boolean): Block|null {\n const parentBlock = this.getSourceBlock();\n const shadowState = this.getShadowState();\n const shadowDom = this.getShadowDom();\n if (parentBlock.isDeadOrDying() || !shadowState && !shadowDom) {\n return null;\n }\n\n let blockShadow;\n if (shadowState) {\n blockShadow = blocks.appendInternal(shadowState, parentBlock.workspace, {\n parentConnection: attemptToConnect ? this : undefined,\n isShadow: true,\n recordUndo: false,\n });\n return blockShadow;\n }\n\n if (shadowDom) {\n blockShadow = Xml.domToBlock(shadowDom, parentBlock.workspace);\n if (attemptToConnect) {\n if (this.type === ConnectionType.INPUT_VALUE) {\n if (!blockShadow.outputConnection) {\n throw new Error('Shadow block is missing an output connection');\n }\n if (!this.connect(blockShadow.outputConnection)) {\n throw new Error('Could not connect shadow block to connection');\n }\n } else if (this.type === ConnectionType.NEXT_STATEMENT) {\n if (!blockShadow.previousConnection) {\n throw new Error('Shadow block is missing previous connection');\n }\n if (!this.connect(blockShadow.previousConnection)) {\n throw new Error('Could not connect shadow block to connection');\n }\n } else {\n throw new Error(\n 'Cannot connect a shadow block to a previous/output connection');\n }\n }\n return blockShadow;\n }\n return null;\n }\n\n /**\n * Saves the given shadow block to both the shadowDom_ and shadowState_\n * properties, in their respective serialized forms.\n *\n * @param shadow The shadow to serialize, or null.\n */\n private serializeShadow_(shadow: Block|null) {\n if (!shadow) {\n return;\n }\n this.shadowDom_ = Xml.blockToDom(shadow) as Element;\n this.shadowState_ = blocks.save(shadow);\n }\n\n /**\n * Returns the connection (starting at the startBlock) which will accept\n * the given connection. This includes compatible connection types and\n * connection checks.\n *\n * @param startBlock The block on which to start the search.\n * @param orphanConnection The connection that is looking for a home.\n * @returns The suitable connection point on the chain of blocks, or null.\n */\n static getConnectionForOrphanedConnection(\n startBlock: Block, orphanConnection: Connection): Connection|null {\n if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) {\n return getConnectionForOrphanedOutput(\n startBlock, orphanConnection.getSourceBlock());\n }\n // Otherwise we're dealing with a stack.\n const connection = startBlock.lastConnectionInStack(true);\n const checker = orphanConnection.getConnectionChecker();\n if (connection && checker.canConnect(orphanConnection, connection, false)) {\n return connection;\n }\n return null;\n }\n}\n\n/**\n * Update two connections to target each other.\n *\n * @param first The first connection to update.\n * @param second The second connection to update.\n */\nfunction connectReciprocally(first: Connection, second: Connection) {\n if (!first || !second) {\n throw Error('Cannot connect null connections.');\n }\n first.targetConnection = second;\n second.targetConnection = first;\n}\n/**\n * Returns the single connection on the block that will accept the orphaned\n * block, if one can be found. If the block has multiple compatible connections\n * (even if they are filled) this returns null. If the block has no compatible\n * connections, this returns null.\n *\n * @param block The superior block.\n * @param orphanBlock The inferior block.\n * @returns The suitable connection point on 'block', or null.\n */\nfunction getSingleConnection(block: Block, orphanBlock: Block): Connection|\n null {\n let foundConnection = null;\n const output = orphanBlock.outputConnection;\n const typeChecker = output.getConnectionChecker();\n\n for (let i = 0, input; input = block.inputList[i]; i++) {\n const connection = input.connection;\n if (connection && typeChecker.canConnect(output, connection, false)) {\n if (foundConnection) {\n return null; // More than one connection.\n }\n foundConnection = connection;\n }\n }\n return foundConnection;\n}\n\n/**\n * Walks down a row a blocks, at each stage checking if there are any\n * connections that will accept the orphaned block. If at any point there\n * are zero or multiple eligible connections, returns null. Otherwise\n * returns the only input on the last block in the chain.\n * Terminates early for shadow blocks.\n *\n * @param startBlock The block on which to start the search.\n * @param orphanBlock The block that is looking for a home.\n * @returns The suitable connection point on the chain of blocks, or null.\n */\nfunction getConnectionForOrphanedOutput(\n startBlock: Block, orphanBlock: Block): Connection|null {\n let newBlock: Block|null = startBlock;\n let connection;\n while (connection = getSingleConnection(newBlock, orphanBlock)) {\n newBlock = connection.targetBlock();\n if (!newBlock || newBlock.isShadow()) {\n return connection;\n }\n }\n return null;\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * The class representing an AST node.\n * Used to traverse the Blockly AST.\n *\n * @class\n */\nimport * as goog from '../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.ASTNode');\n\nimport type {Block} from '../block.js';\nimport type {Connection} from '../connection.js';\nimport {ConnectionType} from '../connection_type.js';\nimport type {Field} from '../field.js';\nimport type {Input} from '../input.js';\nimport type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js';\nimport type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js';\nimport {Coordinate} from '../utils/coordinate.js';\nimport type {Workspace} from '../workspace.js';\n\n\n/**\n * Class for an AST node.\n * It is recommended that you use one of the createNode methods instead of\n * creating a node directly.\n *\n * @alias Blockly.ASTNode\n */\nexport class ASTNode {\n /**\n * True to navigate to all fields. False to only navigate to clickable fields.\n */\n static NAVIGATE_ALL_FIELDS = false;\n\n /**\n * The default y offset to use when moving the cursor from a stack to the\n * workspace.\n */\n private static readonly DEFAULT_OFFSET_Y: number = -20;\n private readonly type_: string;\n private readonly isConnection_: boolean;\n private readonly location_: IASTNodeLocation;\n\n /** The coordinate on the workspace. */\n // AnyDuringMigration because: Type 'null' is not assignable to type\n // 'Coordinate'.\n private wsCoordinate_: Coordinate = null as AnyDuringMigration;\n\n /**\n * @param type The type of the location.\n * Must be in ASTNode.types.\n * @param location The position in the AST.\n * @param opt_params Optional dictionary of options.\n * @alias Blockly.ASTNode\n */\n constructor(type: string, location: IASTNodeLocation, opt_params?: Params) {\n if (!location) {\n throw Error('Cannot create a node without a location.');\n }\n\n /**\n * The type of the location.\n * One of ASTNode.types\n */\n this.type_ = type;\n\n /** Whether the location points to a connection. */\n this.isConnection_ = ASTNode.isConnectionType_(type);\n\n /** The location of the AST node. */\n this.location_ = location;\n\n this.processParams_(opt_params || null);\n }\n\n /**\n * Parse the optional parameters.\n *\n * @param params The user specified parameters.\n */\n private processParams_(params: Params|null) {\n if (!params) {\n return;\n }\n if (params.wsCoordinate) {\n this.wsCoordinate_ = params.wsCoordinate;\n }\n }\n\n /**\n * Gets the value pointed to by this node.\n * It is the callers responsibility to check the node type to figure out what\n * type of object they get back from this.\n *\n * @returns The current field, connection, workspace, or block the cursor is\n * on.\n */\n getLocation(): IASTNodeLocation {\n return this.location_;\n }\n\n /**\n * The type of the current location.\n * One of ASTNode.types\n *\n * @returns The type of the location.\n */\n getType(): string {\n return this.type_;\n }\n\n /**\n * The coordinate on the workspace.\n *\n * @returns The workspace coordinate or null if the location is not a\n * workspace.\n */\n getWsCoordinate(): Coordinate {\n return this.wsCoordinate_;\n }\n\n /**\n * Whether the node points to a connection.\n *\n * @returns [description]\n * @internal\n */\n isConnection(): boolean {\n return this.isConnection_;\n }\n\n /**\n * Given an input find the next editable field or an input with a non null\n * connection in the same block. The current location must be an input\n * connection.\n *\n * @returns The AST node holding the next field or connection or null if there\n * is no editable field or input connection after the given input.\n */\n private findNextForInput_(): ASTNode|null {\n const location = this.location_ as Connection;\n const parentInput = location.getParentInput();\n const block = parentInput!.getSourceBlock();\n // AnyDuringMigration because: Argument of type 'Input | null' is not\n // assignable to parameter of type 'Input'.\n const curIdx = block!.inputList.indexOf(parentInput as AnyDuringMigration);\n for (let i = curIdx + 1; i < block!.inputList.length; i++) {\n const input = block!.inputList[i];\n const fieldRow = input.fieldRow;\n for (let j = 0; j < fieldRow.length; j++) {\n const field = fieldRow[j];\n if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {\n return ASTNode.createFieldNode(field);\n }\n }\n if (input.connection) {\n return ASTNode.createInputNode(input);\n }\n }\n return null;\n }\n\n /**\n * Given a field find the next editable field or an input with a non null\n * connection in the same block. The current location must be a field.\n *\n * @returns The AST node pointing to the next field or connection or null if\n * there is no editable field or input connection after the given input.\n */\n private findNextForField_(): ASTNode|null {\n const location = this.location_ as Field;\n const input = location.getParentInput();\n const block = location.getSourceBlock();\n const curIdx = block.inputList.indexOf((input));\n let fieldIdx = input.fieldRow.indexOf(location) + 1;\n for (let i = curIdx; i < block.inputList.length; i++) {\n const newInput = block.inputList[i];\n const fieldRow = newInput.fieldRow;\n while (fieldIdx < fieldRow.length) {\n if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {\n return ASTNode.createFieldNode(fieldRow[fieldIdx]);\n }\n fieldIdx++;\n }\n fieldIdx = 0;\n if (newInput.connection) {\n return ASTNode.createInputNode(newInput);\n }\n }\n return null;\n }\n\n /**\n * Given an input find the previous editable field or an input with a non null\n * connection in the same block. The current location must be an input\n * connection.\n *\n * @returns The AST node holding the previous field or connection.\n */\n private findPrevForInput_(): ASTNode|null {\n const location = this.location_ as Connection;\n const parentInput = location.getParentInput();\n const block = parentInput!.getSourceBlock();\n // AnyDuringMigration because: Argument of type 'Input | null' is not\n // assignable to parameter of type 'Input'.\n const curIdx = block!.inputList.indexOf(parentInput as AnyDuringMigration);\n for (let i = curIdx; i >= 0; i--) {\n const input = block!.inputList[i];\n if (input.connection && input !== parentInput) {\n return ASTNode.createInputNode(input);\n }\n const fieldRow = input.fieldRow;\n for (let j = fieldRow.length - 1; j >= 0; j--) {\n const field = fieldRow[j];\n if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {\n return ASTNode.createFieldNode(field);\n }\n }\n }\n return null;\n }\n\n /**\n * Given a field find the previous editable field or an input with a non null\n * connection in the same block. The current location must be a field.\n *\n * @returns The AST node holding the previous input or field.\n */\n private findPrevForField_(): ASTNode|null {\n const location = this.location_ as Field;\n const parentInput = location.getParentInput();\n const block = location.getSourceBlock();\n const curIdx = block.inputList.indexOf((parentInput));\n let fieldIdx = parentInput.fieldRow.indexOf(location) - 1;\n for (let i = curIdx; i >= 0; i--) {\n const input = block.inputList[i];\n if (input.connection && input !== parentInput) {\n return ASTNode.createInputNode(input);\n }\n const fieldRow = input.fieldRow;\n while (fieldIdx > -1) {\n if (fieldRow[fieldIdx].isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {\n return ASTNode.createFieldNode(fieldRow[fieldIdx]);\n }\n fieldIdx--;\n }\n // Reset the fieldIdx to the length of the field row of the previous\n // input.\n if (i - 1 >= 0) {\n fieldIdx = block.inputList[i - 1].fieldRow.length - 1;\n }\n }\n return null;\n }\n\n /**\n * Navigate between stacks of blocks on the workspace.\n *\n * @param forward True to go forward. False to go backwards.\n * @returns The first block of the next stack or null if there are no blocks\n * on the workspace.\n */\n private navigateBetweenStacks_(forward: boolean): ASTNode|null {\n let curLocation = this.getLocation();\n // TODO(#6097): Use instanceof checks to exit early for values of\n // curLocation that don't make sense.\n if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) {\n curLocation = (curLocation as IASTNodeLocationWithBlock).getSourceBlock();\n }\n // TODO(#6097): Use instanceof checks to exit early for values of\n // curLocation that don't make sense.\n const curLocationAsBlock = curLocation as Block;\n if (!curLocationAsBlock || curLocationAsBlock.isDeadOrDying()) {\n return null;\n }\n const curRoot = curLocationAsBlock.getRootBlock();\n const topBlocks = curRoot.workspace.getTopBlocks(true);\n for (let i = 0; i < topBlocks.length; i++) {\n const topBlock = topBlocks[i];\n if (curRoot.id === topBlock.id) {\n const offset = forward ? 1 : -1;\n const resultIndex = i + offset;\n if (resultIndex === -1 || resultIndex === topBlocks.length) {\n return null;\n }\n return ASTNode.createStackNode(topBlocks[resultIndex]);\n }\n }\n throw Error(\n 'Couldn\\'t find ' + (forward ? 'next' : 'previous') + ' stack?!');\n }\n\n /**\n * Finds the top most AST node for a given block.\n * This is either the previous connection, output connection or block\n * depending on what kind of connections the block has.\n *\n * @param block The block that we want to find the top connection on.\n * @returns The AST node containing the top connection.\n */\n private findTopASTNodeForBlock_(block: Block): ASTNode|null {\n const topConnection = getParentConnection(block);\n if (topConnection) {\n return ASTNode.createConnectionNode(topConnection);\n } else {\n return ASTNode.createBlockNode(block);\n }\n }\n\n /**\n * Get the AST node pointing to the input that the block is nested under or if\n * the block is not nested then get the stack AST node.\n *\n * @param block The source block of the current location.\n * @returns The AST node pointing to the input connection or the top block of\n * the stack this block is in.\n */\n private getOutAstNodeForBlock_(block: Block): ASTNode|null {\n if (!block) {\n return null;\n }\n // If the block doesn't have a previous connection then it is the top of the\n // substack.\n const topBlock = block.getTopStackBlock();\n const topConnection = getParentConnection(topBlock);\n // If the top connection has a parentInput, create an AST node pointing to\n // that input.\n if (topConnection && topConnection.targetConnection &&\n topConnection.targetConnection.getParentInput()) {\n // AnyDuringMigration because: Argument of type 'Input | null' is not\n // assignable to parameter of type 'Input'.\n return ASTNode.createInputNode(\n topConnection.targetConnection.getParentInput() as\n AnyDuringMigration);\n } else {\n // Go to stack level if you are not underneath an input.\n return ASTNode.createStackNode(topBlock);\n }\n }\n\n /**\n * Find the first editable field or input with a connection on a given block.\n *\n * @param block The source block of the current location.\n * @returns An AST node pointing to the first field or input.\n * Null if there are no editable fields or inputs with connections on the\n * block.\n */\n private findFirstFieldOrInput_(block: Block): ASTNode|null {\n const inputs = block.inputList;\n for (let i = 0; i < inputs.length; i++) {\n const input = inputs[i];\n const fieldRow = input.fieldRow;\n for (let j = 0; j < fieldRow.length; j++) {\n const field = fieldRow[j];\n if (field.isClickable() || ASTNode.NAVIGATE_ALL_FIELDS) {\n return ASTNode.createFieldNode(field);\n }\n }\n if (input.connection) {\n return ASTNode.createInputNode(input);\n }\n }\n return null;\n }\n\n /**\n * Finds the source block of the location of this node.\n *\n * @returns The source block of the location, or null if the node is of type\n * workspace.\n */\n getSourceBlock(): Block|null {\n if (this.getType() === ASTNode.types.BLOCK) {\n return this.getLocation() as Block;\n } else if (this.getType() === ASTNode.types.STACK) {\n return this.getLocation() as Block;\n } else if (this.getType() === ASTNode.types.WORKSPACE) {\n return null;\n } else {\n return (this.getLocation() as IASTNodeLocationWithBlock).getSourceBlock();\n }\n }\n\n /**\n * Find the element to the right of the current element in the AST.\n *\n * @returns An AST node that wraps the next field, connection, block, or\n * workspace. Or null if there is no node to the right.\n */\n next(): ASTNode|null {\n switch (this.type_) {\n case ASTNode.types.STACK:\n return this.navigateBetweenStacks_(true);\n\n case ASTNode.types.OUTPUT: {\n const connection = this.location_ as Connection;\n return ASTNode.createBlockNode(connection.getSourceBlock());\n }\n case ASTNode.types.FIELD:\n return this.findNextForField_();\n\n case ASTNode.types.INPUT:\n return this.findNextForInput_();\n\n case ASTNode.types.BLOCK: {\n const block = this.location_ as Block;\n const nextConnection = block.nextConnection;\n return ASTNode.createConnectionNode(nextConnection);\n }\n case ASTNode.types.PREVIOUS: {\n const connection = this.location_ as Connection;\n return ASTNode.createBlockNode(connection.getSourceBlock());\n }\n case ASTNode.types.NEXT: {\n const connection = this.location_ as Connection;\n const targetConnection = connection.targetConnection;\n return ASTNode.createConnectionNode(targetConnection!);\n }\n }\n\n return null;\n }\n\n /**\n * Find the element one level below and all the way to the left of the current\n * location.\n *\n * @returns An AST node that wraps the next field, connection, workspace, or\n * block. Or null if there is nothing below this node.\n */\n in(): ASTNode|null {\n switch (this.type_) {\n case ASTNode.types.WORKSPACE: {\n const workspace = this.location_ as Workspace;\n const topBlocks = workspace.getTopBlocks(true);\n if (topBlocks.length > 0) {\n return ASTNode.createStackNode(topBlocks[0]);\n }\n break;\n }\n case ASTNode.types.STACK: {\n const block = this.location_ as Block;\n return this.findTopASTNodeForBlock_(block);\n }\n case ASTNode.types.BLOCK: {\n const block = this.location_ as Block;\n return this.findFirstFieldOrInput_(block);\n }\n case ASTNode.types.INPUT: {\n const connection = this.location_ as Connection;\n const targetConnection = connection.targetConnection;\n return ASTNode.createConnectionNode(targetConnection!);\n }\n }\n\n return null;\n }\n\n /**\n * Find the element to the left of the current element in the AST.\n *\n * @returns An AST node that wraps the previous field, connection, workspace\n * or block. Or null if no node exists to the left. null.\n */\n prev(): ASTNode|null {\n switch (this.type_) {\n case ASTNode.types.STACK:\n return this.navigateBetweenStacks_(false);\n\n case ASTNode.types.OUTPUT:\n return null;\n\n case ASTNode.types.FIELD:\n return this.findPrevForField_();\n\n case ASTNode.types.INPUT:\n return this.findPrevForInput_();\n\n case ASTNode.types.BLOCK: {\n const block = this.location_ as Block;\n const topConnection = getParentConnection(block);\n return ASTNode.createConnectionNode(topConnection);\n }\n case ASTNode.types.PREVIOUS: {\n const connection = this.location_ as Connection;\n const targetConnection = connection.targetConnection;\n if (targetConnection && !targetConnection.getParentInput()) {\n return ASTNode.createConnectionNode(targetConnection);\n }\n break;\n }\n case ASTNode.types.NEXT: {\n const connection = this.location_ as Connection;\n return ASTNode.createBlockNode(connection.getSourceBlock());\n }\n }\n\n return null;\n }\n\n /**\n * Find the next element that is one position above and all the way to the\n * left of the current location.\n *\n * @returns An AST node that wraps the next field, connection, workspace or\n * block. Or null if we are at the workspace level.\n */\n out(): ASTNode|null {\n switch (this.type_) {\n case ASTNode.types.STACK: {\n const block = this.location_ as Block;\n const blockPos = block.getRelativeToSurfaceXY();\n // TODO: Make sure this is in the bounds of the workspace.\n const wsCoordinate =\n new Coordinate(blockPos.x, blockPos.y + ASTNode.DEFAULT_OFFSET_Y);\n return ASTNode.createWorkspaceNode(block.workspace, wsCoordinate);\n }\n case ASTNode.types.OUTPUT: {\n const connection = this.location_ as Connection;\n const target = connection.targetConnection;\n if (target) {\n return ASTNode.createConnectionNode(target);\n }\n return ASTNode.createStackNode(connection.getSourceBlock());\n }\n case ASTNode.types.FIELD: {\n const field = this.location_ as Field;\n return ASTNode.createBlockNode(field.getSourceBlock());\n }\n case ASTNode.types.INPUT: {\n const connection = this.location_ as Connection;\n return ASTNode.createBlockNode(connection.getSourceBlock());\n }\n case ASTNode.types.BLOCK: {\n const block = this.location_ as Block;\n return this.getOutAstNodeForBlock_(block);\n }\n case ASTNode.types.PREVIOUS: {\n const connection = this.location_ as Connection;\n return this.getOutAstNodeForBlock_(connection.getSourceBlock());\n }\n case ASTNode.types.NEXT: {\n const connection = this.location_ as Connection;\n return this.getOutAstNodeForBlock_(connection.getSourceBlock());\n }\n }\n\n return null;\n }\n\n /**\n * Whether an AST node of the given type points to a connection.\n *\n * @param type The type to check. One of ASTNode.types.\n * @returns True if a node of the given type points to a connection.\n */\n private static isConnectionType_(type: string): boolean {\n switch (type) {\n case ASTNode.types.PREVIOUS:\n case ASTNode.types.NEXT:\n case ASTNode.types.INPUT:\n case ASTNode.types.OUTPUT:\n return true;\n }\n return false;\n }\n\n /**\n * Create an AST node pointing to a field.\n *\n * @param field The location of the AST node.\n * @returns An AST node pointing to a field.\n */\n static createFieldNode(field: Field): ASTNode|null {\n if (!field) {\n return null;\n }\n return new ASTNode(ASTNode.types.FIELD, field);\n }\n\n /**\n * Creates an AST node pointing to a connection. If the connection has a\n * parent input then create an AST node of type input that will hold the\n * connection.\n *\n * @param connection This is the connection the node will point to.\n * @returns An AST node pointing to a connection.\n */\n static createConnectionNode(connection: Connection): ASTNode|null {\n if (!connection) {\n return null;\n }\n const type = connection.type;\n if (type === ConnectionType.INPUT_VALUE) {\n // AnyDuringMigration because: Argument of type 'Input | null' is not\n // assignable to parameter of type 'Input'.\n return ASTNode.createInputNode(\n connection.getParentInput() as AnyDuringMigration);\n } else if (\n type === ConnectionType.NEXT_STATEMENT && connection.getParentInput()) {\n // AnyDuringMigration because: Argument of type 'Input | null' is not\n // assignable to parameter of type 'Input'.\n return ASTNode.createInputNode(\n connection.getParentInput() as AnyDuringMigration);\n } else if (type === ConnectionType.NEXT_STATEMENT) {\n return new ASTNode(ASTNode.types.NEXT, connection);\n } else if (type === ConnectionType.OUTPUT_VALUE) {\n return new ASTNode(ASTNode.types.OUTPUT, connection);\n } else if (type === ConnectionType.PREVIOUS_STATEMENT) {\n return new ASTNode(ASTNode.types.PREVIOUS, connection);\n }\n return null;\n }\n\n /**\n * Creates an AST node pointing to an input. Stores the input connection as\n * the location.\n *\n * @param input The input used to create an AST node.\n * @returns An AST node pointing to a input.\n */\n static createInputNode(input: Input): ASTNode|null {\n if (!input || !input.connection) {\n return null;\n }\n return new ASTNode(ASTNode.types.INPUT, input.connection);\n }\n\n /**\n * Creates an AST node pointing to a block.\n *\n * @param block The block used to create an AST node.\n * @returns An AST node pointing to a block.\n */\n static createBlockNode(block: Block): ASTNode|null {\n if (!block) {\n return null;\n }\n return new ASTNode(ASTNode.types.BLOCK, block);\n }\n\n /**\n * Create an AST node of type stack. A stack, represented by its top block, is\n * the set of all blocks connected to a top block, including the top\n * block.\n *\n * @param topBlock A top block has no parent and can be found in the list\n * returned by workspace.getTopBlocks().\n * @returns An AST node of type stack that points to the top block on the\n * stack.\n */\n static createStackNode(topBlock: Block): ASTNode|null {\n if (!topBlock) {\n return null;\n }\n return new ASTNode(ASTNode.types.STACK, topBlock);\n }\n\n /**\n * Creates an AST node pointing to a workspace.\n *\n * @param workspace The workspace that we are on.\n * @param wsCoordinate The position on the workspace for this node.\n * @returns An AST node pointing to a workspace and a position on the\n * workspace.\n */\n static createWorkspaceNode(\n workspace: Workspace|null, wsCoordinate: Coordinate|null): ASTNode|null {\n if (!wsCoordinate || !workspace) {\n return null;\n }\n const params = {wsCoordinate};\n return new ASTNode(ASTNode.types.WORKSPACE, workspace, params);\n }\n\n /**\n * Creates an AST node for the top position on a block.\n * This is either an output connection, previous connection, or block.\n *\n * @param block The block to find the top most AST node on.\n * @returns The AST node holding the top most position on the block.\n */\n static createTopNode(block: Block): ASTNode|null {\n let astNode;\n const topConnection = getParentConnection(block);\n if (topConnection) {\n astNode = ASTNode.createConnectionNode(topConnection);\n } else {\n astNode = ASTNode.createBlockNode(block);\n }\n return astNode;\n }\n}\n\nexport namespace ASTNode {\n export interface Params {\n wsCoordinate: Coordinate;\n }\n\n export enum types {\n FIELD = 'field',\n BLOCK = 'block',\n INPUT = 'input',\n OUTPUT = 'output',\n NEXT = 'next',\n PREVIOUS = 'previous',\n STACK = 'stack',\n WORKSPACE = 'workspace',\n }\n}\n\nexport type Params = ASTNode.Params;\n// No need to export ASTNode.types from the module at this time because (1) it\n// wasn't automatically converted by the automatic migration script, (2) the\n// name doesn't follow the styleguide.\n\n\n/**\n * Gets the parent connection on a block.\n * This is either an output connection, previous connection or undefined.\n * If both connections exist return the one that is actually connected\n * to another block.\n *\n * @param block The block to find the parent connection on.\n * @returns The connection connecting to the parent of the block.\n */\nfunction getParentConnection(block: Block): Connection {\n let topConnection = block.outputConnection;\n if (!topConnection ||\n block.previousConnection && block.previousConnection.isConnected()) {\n topConnection = block.previousConnection;\n }\n return topConnection;\n}\n","/**\n * @license\n * Copyright 2018 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Methods animating a block on connection and disconnection.\n *\n * @namespace Blockly.blockAnimations\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.blockAnimations');\n\nimport type {BlockSvg} from './block_svg.js';\nimport * as dom from './utils/dom.js';\nimport {Svg} from './utils/svg.js';\n\n\n/** A bounding box for a cloned block. */\ninterface CloneRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** PID of disconnect UI animation. There can only be one at a time. */\nlet disconnectPid: ReturnType|null = null;\n\n/** SVG group of wobbling block. There can only be one at a time. */\nlet disconnectGroup: SVGElement|null = null;\n\n\n/**\n * Play some UI effects (sound, animation) when disposing of a block.\n *\n * @param block The block being disposed of.\n * @alias Blockly.blockAnimations.disposeUiEffect\n * @internal\n */\nexport function disposeUiEffect(block: BlockSvg) {\n const workspace = block.workspace;\n const svgGroup = block.getSvgRoot();\n workspace.getAudioManager().play('delete');\n\n const xy = workspace.getSvgXY(svgGroup);\n // Deeply clone the current block.\n const clone: SVGGElement = svgGroup.cloneNode(true) as SVGGElement;\n clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');\n workspace.getParentSvg().appendChild(clone);\n const cloneRect =\n {'x': xy.x, 'y': xy.y, 'width': block.width, 'height': block.height};\n disposeUiStep(clone, cloneRect, workspace.RTL, new Date(), workspace.scale);\n}\n/**\n * Animate a cloned block and eventually dispose of it.\n * This is a class method, not an instance method since the original block has\n * been destroyed and is no longer accessible.\n *\n * @param clone SVG element to animate and dispose of.\n * @param rect Starting rect of the clone.\n * @param rtl True if RTL, false if LTR.\n * @param start Date of animation's start.\n * @param workspaceScale Scale of workspace.\n */\nfunction disposeUiStep(\n clone: Element, rect: CloneRect, rtl: boolean, start: Date,\n workspaceScale: number) {\n const ms = new Date().getTime() - start.getTime();\n const percent = ms / 150;\n if (percent > 1) {\n dom.removeNode(clone);\n } else {\n const x =\n rect.x + (rtl ? -1 : 1) * rect.width * workspaceScale / 2 * percent;\n const y = rect.y + rect.height * workspaceScale * percent;\n const scale = (1 - percent) * workspaceScale;\n clone.setAttribute(\n 'transform',\n 'translate(' + x + ',' + y + ')' +\n ' scale(' + scale + ')');\n setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale);\n }\n}\n\n/**\n * Play some UI effects (sound, ripple) after a connection has been established.\n *\n * @param block The block being connected.\n * @alias Blockly.blockAnimations.connectionUiEffect\n * @internal\n */\nexport function connectionUiEffect(block: BlockSvg) {\n const workspace = block.workspace;\n const scale = workspace.scale;\n workspace.getAudioManager().play('click');\n if (scale < 1) {\n return; // Too small to care about visual effects.\n }\n // Determine the absolute coordinates of the inferior block.\n const xy = workspace.getSvgXY(block.getSvgRoot());\n // Offset the coordinates based on the two connection types, fix scale.\n if (block.outputConnection) {\n xy.x += (block.RTL ? 3 : -3) * scale;\n xy.y += 13 * scale;\n } else if (block.previousConnection) {\n xy.x += (block.RTL ? -23 : 23) * scale;\n xy.y += 3 * scale;\n }\n const ripple = dom.createSvgElement(\n Svg.CIRCLE, {\n 'cx': xy.x,\n 'cy': xy.y,\n 'r': 0,\n 'fill': 'none',\n 'stroke': '#888',\n 'stroke-width': 10,\n },\n workspace.getParentSvg());\n // Start the animation.\n connectionUiStep(ripple, new Date(), scale);\n}\n\n/**\n * Expand a ripple around a connection.\n *\n * @param ripple Element to animate.\n * @param start Date of animation's start.\n * @param scale Scale of workspace.\n */\nfunction connectionUiStep(ripple: SVGElement, start: Date, scale: number) {\n const ms = new Date().getTime() - start.getTime();\n const percent = ms / 150;\n if (percent > 1) {\n dom.removeNode(ripple);\n } else {\n ripple.setAttribute('r', (percent * 25 * scale).toString());\n ripple.style.opacity = (1 - percent).toString();\n disconnectPid = setTimeout(connectionUiStep, 10, ripple, start, scale);\n }\n}\n\n/**\n * Play some UI effects (sound, animation) when disconnecting a block.\n *\n * @param block The block being disconnected.\n * @alias Blockly.blockAnimations.disconnectUiEffect\n * @internal\n */\nexport function disconnectUiEffect(block: BlockSvg) {\n disconnectUiStop();\n block.workspace.getAudioManager().play('disconnect');\n if (block.workspace.scale < 1) {\n return; // Too small to care about visual effects.\n }\n // Horizontal distance for bottom of block to wiggle.\n const DISPLACEMENT = 10;\n // Scale magnitude of skew to height of block.\n const height = block.getHeightWidth().height;\n let magnitude = Math.atan(DISPLACEMENT / height) / Math.PI * 180;\n if (!block.RTL) {\n magnitude *= -1;\n }\n // Start the animation.\n disconnectGroup = block.getSvgRoot();\n disconnectUiStep(disconnectGroup, magnitude, new Date());\n}\n\n/**\n * Animate a brief wiggle of a disconnected block.\n *\n * @param group SVG element to animate.\n * @param magnitude Maximum degrees skew (reversed for RTL).\n * @param start Date of animation's start.\n */\nfunction disconnectUiStep(group: SVGElement, magnitude: number, start: Date) {\n const DURATION = 200; // Milliseconds.\n const WIGGLES = 3; // Half oscillations.\n\n const ms = new Date().getTime() - start.getTime();\n const percent = ms / DURATION;\n\n let skew = '';\n if (percent <= 1) {\n const val = Math.round(\n Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude);\n skew = `skewX(${val})`;\n disconnectPid = setTimeout(disconnectUiStep, 10, group, magnitude, start);\n }\n group.setAttribute('transform', skew);\n}\n\n/**\n * Stop the disconnect UI animation immediately.\n *\n * @alias Blockly.blockAnimations.disconnectUiStop\n * @internal\n */\nexport function disconnectUiStop() {\n if (disconnectGroup) {\n if (disconnectPid) {\n clearTimeout(disconnectPid);\n }\n disconnectGroup.setAttribute('transform', '');\n disconnectGroup = null;\n }\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Blockly's internal clipboard for managing copy-paste.\n *\n * @namespace Blockly.clipboard\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.clipboard');\n\nimport type {CopyData, ICopyable} from './interfaces/i_copyable.js';\n\n\n/** Metadata about the object that is currently on the clipboard. */\nlet copyData: CopyData|null = null;\n\n/**\n * Copy a block or workspace comment onto the local clipboard.\n *\n * @param toCopy Block or Workspace Comment to be copied.\n * @alias Blockly.clipboard.copy\n * @internal\n */\nexport function copy(toCopy: ICopyable) {\n TEST_ONLY.copyInternal(toCopy);\n}\n\n/**\n * Private version of copy for stubbing in tests.\n */\nfunction copyInternal(toCopy: ICopyable) {\n copyData = toCopy.toCopyData();\n}\n\n/**\n * Paste a block or workspace comment on to the main workspace.\n *\n * @returns The pasted thing if the paste was successful, null otherwise.\n * @alias Blockly.clipboard.paste\n * @internal\n */\nexport function paste(): ICopyable|null {\n if (!copyData) {\n return null;\n }\n // Pasting always pastes to the main workspace, even if the copy\n // started in a flyout workspace.\n let workspace = copyData.source;\n if (workspace.isFlyout) {\n workspace = workspace.targetWorkspace!;\n }\n if (copyData.typeCounts &&\n workspace.isCapacityAvailable(copyData.typeCounts)) {\n return workspace.paste(copyData.saveInfo);\n }\n return null;\n}\n\n/**\n * Duplicate this block and its children, or a workspace comment.\n *\n * @param toDuplicate Block or Workspace Comment to be duplicated.\n * @returns The block or workspace comment that was duplicated, or null if the\n * duplication failed.\n * @alias Blockly.clipboard.duplicate\n * @internal\n */\nexport function duplicate(toDuplicate: ICopyable): ICopyable|null {\n return TEST_ONLY.duplicateInternal(toDuplicate);\n}\n\n/**\n * Private version of duplicate for stubbing in tests.\n */\nfunction duplicateInternal(toDuplicate: ICopyable): ICopyable|null {\n const oldCopyData = copyData;\n copy(toDuplicate);\n const pastedThing =\n toDuplicate.toCopyData()?.source?.paste(copyData!.saveInfo) ?? null;\n copyData = oldCopyData;\n return pastedThing;\n}\n\nexport const TEST_ONLY = {\n duplicateInternal,\n copyInternal,\n};\n","/**\n * @license\n * Copyright 2011 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Functionality for the right-click context menus.\n *\n * @namespace Blockly.ContextMenu\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.ContextMenu');\n\nimport type {Block} from './block.js';\nimport type {BlockSvg} from './block_svg.js';\nimport * as browserEvents from './browser_events.js';\nimport * as clipboard from './clipboard.js';\nimport {config} from './config.js';\nimport * as dom from './utils/dom.js';\nimport type {ContextMenuOption, LegacyContextMenuOption} from './contextmenu_registry.js';\nimport * as eventUtils from './events/utils.js';\nimport {Menu} from './menu.js';\nimport {MenuItem} from './menuitem.js';\nimport {Msg} from './msg.js';\nimport * as aria from './utils/aria.js';\nimport {Coordinate} from './utils/coordinate.js';\nimport {Rect} from './utils/rect.js';\nimport * as svgMath from './utils/svg_math.js';\nimport * as WidgetDiv from './widgetdiv.js';\nimport {WorkspaceCommentSvg} from './workspace_comment_svg.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\nimport * as Xml from './xml.js';\n\n\n/**\n * Which block is the context menu attached to?\n */\nlet currentBlock: Block|null = null;\n\nconst dummyOwner = {};\n\n/**\n * Gets the block the context menu is currently attached to.\n *\n * @returns The block the context menu is attached to.\n * @alias Blockly.ContextMenu.getCurrentBlock\n */\nexport function getCurrentBlock(): Block|null {\n return currentBlock;\n}\n\n/**\n * Sets the block the context menu is currently attached to.\n *\n * @param block The block the context menu is attached to.\n * @alias Blockly.ContextMenu.setCurrentBlock\n */\nexport function setCurrentBlock(block: Block|null) {\n currentBlock = block;\n}\n\n/**\n * Menu object.\n */\nlet menu_: Menu|null = null;\n\n/**\n * Construct the menu based on the list of options and show the menu.\n *\n * @param e Mouse event.\n * @param options Array of menu options.\n * @param rtl True if RTL, false if LTR.\n * @alias Blockly.ContextMenu.show\n */\nexport function show(\n e: Event, options: (ContextMenuOption|LegacyContextMenuOption)[],\n rtl: boolean) {\n WidgetDiv.show(dummyOwner, rtl, dispose);\n if (!options.length) {\n hide();\n return;\n }\n const menu = populate_(options, rtl);\n menu_ = menu;\n\n position_(menu, e, rtl);\n // 1ms delay is required for focusing on context menus because some other\n // mouse event is still waiting in the queue and clears focus.\n setTimeout(function() {\n menu.focus();\n }, 1);\n currentBlock = null; // May be set by Blockly.Block.\n}\n\n/**\n * Create the context menu object and populate it with the given options.\n *\n * @param options Array of menu options.\n * @param rtl True if RTL, false if LTR.\n * @returns The menu that will be shown on right click.\n */\nfunction populate_(\n options: (ContextMenuOption|LegacyContextMenuOption)[],\n rtl: boolean): Menu {\n /* Here's what one option object looks like:\n {text: 'Make It So',\n enabled: true,\n callback: Blockly.MakeItSo}\n */\n const menu = new Menu();\n menu.setRole(aria.Role.MENU);\n for (let i = 0; i < options.length; i++) {\n const option = options[i];\n const menuItem = new MenuItem(option.text);\n menuItem.setRightToLeft(rtl);\n menuItem.setRole(aria.Role.MENUITEM);\n menu.addChild(menuItem);\n menuItem.setEnabled(option.enabled);\n if (option.enabled) {\n const actionHandler = function() {\n hide();\n // If .scope does not exist on the option, then the callback will not\n // be expecting a scope parameter, so there should be no problems. Just\n // assume it is a ContextMenuOption and we'll pass undefined if it's\n // not.\n option.callback((option as ContextMenuOption).scope);\n };\n menuItem.onAction(actionHandler, {});\n }\n }\n return menu;\n}\n\n/**\n * Add the menu to the page and position it correctly.\n *\n * @param menu The menu to add and position.\n * @param e Mouse event for the right click that is making the context\n * menu appear.\n * @param rtl True if RTL, false if LTR.\n */\nfunction position_(menu: Menu, e: Event, rtl: boolean) {\n // Record windowSize and scrollOffset before adding menu.\n const viewportBBox = svgMath.getViewportBBox();\n const mouseEvent = e as MouseEvent;\n // This one is just a point, but we'll pretend that it's a rect so we can use\n // some helper functions.\n const anchorBBox = new Rect(\n mouseEvent.clientY + viewportBBox.top,\n mouseEvent.clientY + viewportBBox.top,\n mouseEvent.clientX + viewportBBox.left,\n mouseEvent.clientX + viewportBBox.left);\n\n createWidget_(menu);\n const menuSize = menu.getSize();\n\n if (rtl) {\n anchorBBox.left += menuSize.width;\n anchorBBox.right += menuSize.width;\n viewportBBox.left += menuSize.width;\n viewportBBox.right += menuSize.width;\n }\n\n WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, rtl);\n // Calling menuDom.focus() has to wait until after the menu has been placed\n // correctly. Otherwise it will cause a page scroll to get the misplaced menu\n // in view. See issue #1329.\n menu.focus();\n}\n\n/**\n * Create and render the menu widget inside Blockly's widget div.\n *\n * @param menu The menu to add to the widget div.\n */\nfunction createWidget_(menu: Menu) {\n const div = WidgetDiv.getDiv();\n if (!div) {\n throw Error('Attempting to create a context menu when widget div is null');\n }\n const menuDom = menu.render(div);\n dom.addClass(menuDom, 'blocklyContextMenu');\n // Prevent system context menu when right-clicking a Blockly context menu.\n browserEvents.conditionalBind(\n (menuDom as EventTarget), 'contextmenu', null, haltPropagation);\n // Focus only after the initial render to avoid issue #1329.\n menu.focus();\n}\n/**\n * Halts the propagation of the event without doing anything else.\n *\n * @param e An event.\n */\nfunction haltPropagation(e: Event) {\n // This event has been handled. No need to bubble up to the document.\n e.preventDefault();\n e.stopPropagation();\n}\n\n/**\n * Hide the context menu.\n *\n * @alias Blockly.ContextMenu.hide\n */\nexport function hide() {\n WidgetDiv.hideIfOwner(dummyOwner);\n currentBlock = null;\n}\n\n/**\n * Dispose of the menu.\n *\n * @alias Blockly.ContextMenu.dispose\n */\nexport function dispose() {\n if (menu_) {\n menu_.dispose();\n menu_ = null;\n }\n}\n\n/**\n * Create a callback function that creates and configures a block,\n * then places the new block next to the original.\n *\n * @param block Original block.\n * @param xml XML representation of new block.\n * @returns Function that creates a block.\n * @alias Blockly.ContextMenu.callbackFactory\n */\nexport function callbackFactory(block: Block, xml: Element): Function {\n return () => {\n eventUtils.disable();\n let newBlock;\n try {\n newBlock = Xml.domToBlock(xml, block.workspace!) as BlockSvg;\n // Move the new block next to the old block.\n const xy = block.getRelativeToSurfaceXY();\n if (block.RTL) {\n xy.x -= config.snapRadius;\n } else {\n xy.x += config.snapRadius;\n }\n xy.y += config.snapRadius * 2;\n newBlock.moveBy(xy.x, xy.y);\n } finally {\n eventUtils.enable();\n }\n if (eventUtils.isEnabled() && !newBlock.isShadow()) {\n eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));\n }\n newBlock.select();\n };\n}\n\n// Helper functions for creating context menu options.\n\n/**\n * Make a context menu option for deleting the current workspace comment.\n *\n * @param comment The workspace comment where the\n * right-click originated.\n * @returns A menu option,\n * containing text, enabled, and a callback.\n * @alias Blockly.ContextMenu.commentDeleteOption\n * @internal\n */\nexport function commentDeleteOption(comment: WorkspaceCommentSvg):\n LegacyContextMenuOption {\n const deleteOption = {\n text: Msg['REMOVE_COMMENT'],\n enabled: true,\n callback: function() {\n eventUtils.setGroup(true);\n comment.dispose();\n eventUtils.setGroup(false);\n },\n };\n return deleteOption;\n}\n\n/**\n * Make a context menu option for duplicating the current workspace comment.\n *\n * @param comment The workspace comment where the\n * right-click originated.\n * @returns A menu option,\n * containing text, enabled, and a callback.\n * @alias Blockly.ContextMenu.commentDuplicateOption\n * @internal\n */\nexport function commentDuplicateOption(comment: WorkspaceCommentSvg):\n LegacyContextMenuOption {\n const duplicateOption = {\n text: Msg['DUPLICATE_COMMENT'],\n enabled: true,\n callback: function() {\n clipboard.duplicate(comment);\n },\n };\n return duplicateOption;\n}\n\n/**\n * Make a context menu option for adding a comment on the workspace.\n *\n * @param ws The workspace where the right-click\n * originated.\n * @param e The right-click mouse event.\n * @returns A menu option, containing text, enabled, and a callback.\n * @suppress {strictModuleDepCheck,checkTypes} Suppress checks while workspace\n * comments are not bundled in.\n * @alias Blockly.ContextMenu.workspaceCommentOption\n * @internal\n */\nexport function workspaceCommentOption(\n ws: WorkspaceSvg, e: Event): ContextMenuOption {\n /**\n * Helper function to create and position a comment correctly based on the\n * location of the mouse event.\n */\n function addWsComment() {\n const comment = new WorkspaceCommentSvg(\n ws, Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],\n WorkspaceCommentSvg.DEFAULT_SIZE, WorkspaceCommentSvg.DEFAULT_SIZE);\n\n const injectionDiv = ws.getInjectionDiv();\n // Bounding rect coordinates are in client coordinates, meaning that they\n // are in pixels relative to the upper left corner of the visible browser\n // window. These coordinates change when you scroll the browser window.\n const boundingRect = injectionDiv.getBoundingClientRect();\n\n // The client coordinates offset by the injection div's upper left corner.\n const mouseEvent = e as MouseEvent;\n const clientOffsetPixels = new Coordinate(\n mouseEvent.clientX - boundingRect.left,\n mouseEvent.clientY - boundingRect.top);\n\n // The offset in pixels between the main workspace's origin and the upper\n // left corner of the injection div.\n const mainOffsetPixels = ws.getOriginOffsetInPixels();\n\n // The position of the new comment in pixels relative to the origin of the\n // main workspace.\n const finalOffset =\n Coordinate.difference(clientOffsetPixels, mainOffsetPixels);\n // The position of the new comment in main workspace coordinates.\n finalOffset.scale(1 / ws.scale);\n\n const commentX = finalOffset.x;\n const commentY = finalOffset.y;\n comment.moveBy(commentX, commentY);\n if (ws.rendered) {\n comment.initSvg();\n comment.render();\n comment.select();\n }\n }\n\n const wsCommentOption = {\n enabled: true,\n } as ContextMenuOption;\n wsCommentOption.text = Msg['ADD_COMMENT'];\n wsCommentOption.callback = function() {\n addWsComment();\n };\n return wsCommentOption;\n}\n","/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility functions for positioning UI elements.\n *\n * @namespace Blockly.uiPosition\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.uiPosition');\n\nimport type {UiMetrics} from './metrics_manager.js';\nimport {Scrollbar} from './scrollbar.js';\nimport {Rect} from './utils/rect.js';\nimport type {Size} from './utils/size.js';\nimport * as toolbox from './utils/toolbox.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\n/**\n * Enum for vertical positioning.\n *\n * @alias Blockly.uiPosition.verticalPosition\n * @internal\n */\nexport enum verticalPosition {\n TOP,\n BOTTOM\n}\n\n/**\n * Enum for horizontal positioning.\n *\n * @alias Blockly.uiPosition.horizontalPosition\n * @internal\n */\nexport enum horizontalPosition {\n LEFT,\n RIGHT\n}\n\n/**\n * An object defining a horizontal and vertical positioning.\n *\n * @alias Blockly.uiPosition.Position\n * @internal\n */\nexport interface Position {\n horizontal: horizontalPosition;\n vertical: verticalPosition;\n}\n\n/**\n * Enum for bump rules to use for dealing with collisions.\n *\n * @alias Blockly.uiPosition.bumpDirection\n * @internal\n */\nexport enum bumpDirection {\n UP,\n DOWN\n}\n\n/**\n * Returns a rectangle representing reasonable position for where to place a UI\n * element of the specified size given the restraints and locations of the\n * scrollbars. This method does not take into account any already placed UI\n * elements.\n *\n * @param position The starting horizontal and vertical position.\n * @param size the size of the UI element to get a start position for.\n * @param horizontalPadding The horizontal padding to use.\n * @param verticalPadding The vertical padding to use.\n * @param metrics The workspace UI metrics.\n * @param workspace The workspace.\n * @returns The suggested start position.\n * @alias Blockly.uiPosition.getStartPositionRect\n * @internal\n */\nexport function getStartPositionRect(\n position: Position, size: Size, horizontalPadding: number,\n verticalPadding: number, metrics: UiMetrics,\n workspace: WorkspaceSvg): Rect {\n // Horizontal positioning.\n let left = 0;\n const hasVerticalScrollbar =\n workspace.scrollbar && workspace.scrollbar.canScrollVertically();\n if (position.horizontal === horizontalPosition.LEFT) {\n left = metrics.absoluteMetrics.left + horizontalPadding;\n if (hasVerticalScrollbar && workspace.RTL) {\n left += Scrollbar.scrollbarThickness;\n }\n } else { // position.horizontal === horizontalPosition.RIGHT\n left = metrics.absoluteMetrics.left + metrics.viewMetrics.width -\n size.width - horizontalPadding;\n if (hasVerticalScrollbar && !workspace.RTL) {\n left -= Scrollbar.scrollbarThickness;\n }\n }\n // Vertical positioning.\n let top = 0;\n if (position.vertical === verticalPosition.TOP) {\n top = metrics.absoluteMetrics.top + verticalPadding;\n } else { // position.vertical === verticalPosition.BOTTOM\n top = metrics.absoluteMetrics.top + metrics.viewMetrics.height -\n size.height - verticalPadding;\n if (workspace.scrollbar && workspace.scrollbar.canScrollHorizontally()) {\n // The scrollbars are always positioned on the bottom if they exist.\n top -= Scrollbar.scrollbarThickness;\n }\n }\n return new Rect(top, top + size.height, left, left + size.width);\n}\n\n/**\n * Returns a corner position that is on the opposite side of the workspace from\n * the toolbox.\n * If in horizontal orientation, defaults to the bottom corner. If in vertical\n * orientation, defaults to the right corner.\n *\n * @param workspace The workspace.\n * @param metrics The workspace metrics.\n * @returns The suggested corner position.\n * @alias Blockly.uiPosition.getCornerOppositeToolbox\n * @internal\n */\nexport function getCornerOppositeToolbox(\n workspace: WorkspaceSvg, metrics: UiMetrics): Position {\n const leftCorner =\n metrics.toolboxMetrics.position !== toolbox.Position.LEFT &&\n (!workspace.horizontalLayout || workspace.RTL);\n const topCorner = metrics.toolboxMetrics.position === toolbox.Position.BOTTOM;\n const hPosition =\n leftCorner ? horizontalPosition.LEFT : horizontalPosition.RIGHT;\n const vPosition = topCorner ? verticalPosition.TOP : verticalPosition.BOTTOM;\n return {horizontal: hPosition, vertical: vPosition};\n}\n\n/**\n * Returns a position Rect based on a starting position that is bumped\n * so that it doesn't intersect with any of the provided savedPositions. This\n * method does not check that the bumped position is still within bounds.\n *\n * @param startRect The starting position to use.\n * @param margin The margin to use between elements when bumping.\n * @param bumpDir The direction to bump if there is a collision with an existing\n * UI element.\n * @param savedPositions List of rectangles that represent the positions of UI\n * elements already placed.\n * @returns The suggested position rectangle.\n * @alias Blockly.uiPosition.bumpPositionRect\n * @internal\n */\nexport function bumpPositionRect(\n startRect: Rect, margin: number, bumpDir: bumpDirection,\n savedPositions: Rect[]): Rect {\n let top = startRect.top;\n const left = startRect.left;\n const width = startRect.right - startRect.left;\n const height = startRect.bottom - startRect.top;\n\n // Check for collision and bump if needed.\n let boundingRect = startRect;\n for (let i = 0; i < savedPositions.length; i++) {\n const otherEl = savedPositions[i];\n if (boundingRect.intersects(otherEl)) {\n if (bumpDir === bumpDirection.UP) {\n top = otherEl.top - height - margin;\n } else { // bumpDir === bumpDirection.DOWN\n top = otherEl.bottom + margin;\n }\n // Recheck other savedPositions\n boundingRect = new Rect(top, top + height, left, left + width);\n i = -1;\n }\n }\n return boundingRect;\n}\n","/**\n * @license\n * Copyright 2020 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Registers default keyboard shortcuts.\n *\n * @namespace Blockly.ShortcutItems\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.ShortcutItems');\n\nimport {BlockSvg} from './block_svg.js';\nimport * as clipboard from './clipboard.js';\nimport * as common from './common.js';\nimport {Gesture} from './gesture.js';\nimport type {ICopyable} from './interfaces/i_copyable.js';\nimport {KeyboardShortcut, ShortcutRegistry} from './shortcut_registry.js';\nimport {KeyCodes} from './utils/keycodes.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\n\n\n/**\n * Object holding the names of the default shortcut items.\n *\n * @alias Blockly.ShortcutItems.names\n */\nexport enum names {\n ESCAPE = 'escape',\n DELETE = 'delete',\n COPY = 'copy',\n CUT = 'cut',\n PASTE = 'paste',\n UNDO = 'undo',\n REDO = 'redo'\n}\n\n/**\n * Keyboard shortcut to hide chaff on escape.\n *\n * @alias Blockly.ShortcutItems.registerEscape\n */\nexport function registerEscape() {\n const escapeAction: KeyboardShortcut = {\n name: names.ESCAPE,\n preconditionFn(workspace) {\n return !workspace.options.readOnly;\n },\n callback(workspace) {\n // AnyDuringMigration because: Property 'hideChaff' does not exist on\n // type 'Workspace'.\n (workspace as AnyDuringMigration).hideChaff();\n return true;\n },\n keyCodes: [KeyCodes.ESC],\n };\n ShortcutRegistry.registry.register(escapeAction);\n}\n\n/**\n * Keyboard shortcut to delete a block on delete or backspace\n *\n * @alias Blockly.ShortcutItems.registerDelete\n */\nexport function registerDelete() {\n const deleteShortcut: KeyboardShortcut = {\n name: names.DELETE,\n preconditionFn(workspace) {\n const selected = common.getSelected();\n return !workspace.options.readOnly && selected != null &&\n selected.isDeletable();\n },\n callback(workspace, e) {\n // Delete or backspace.\n // Stop the browser from going back to the previous page.\n // Do this first to prevent an error in the delete code from resulting in\n // data loss.\n e.preventDefault();\n // Don't delete while dragging. Jeez.\n if (Gesture.inProgress()) {\n return false;\n }\n (common.getSelected() as BlockSvg).checkAndDelete();\n return true;\n },\n keyCodes: [KeyCodes.DELETE, KeyCodes.BACKSPACE],\n };\n ShortcutRegistry.registry.register(deleteShortcut);\n}\n\n/**\n * Keyboard shortcut to copy a block on ctrl+c, cmd+c, or alt+c.\n *\n * @alias Blockly.ShortcutItems.registerCopy\n */\nexport function registerCopy() {\n const ctrlC = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.C, [KeyCodes.CTRL]);\n const altC =\n ShortcutRegistry.registry.createSerializedKey(KeyCodes.C, [KeyCodes.ALT]);\n const metaC = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.C, [KeyCodes.META]);\n\n const copyShortcut: KeyboardShortcut = {\n name: names.COPY,\n preconditionFn(workspace) {\n const selected = common.getSelected();\n return !workspace.options.readOnly && !Gesture.inProgress() &&\n selected != null && selected.isDeletable() && selected.isMovable();\n },\n callback(workspace, e) {\n // Prevent the default copy behavior, which may beep or otherwise indicate\n // an error due to the lack of a selection.\n e.preventDefault();\n // AnyDuringMigration because: Property 'hideChaff' does not exist on\n // type 'Workspace'.\n (workspace as AnyDuringMigration).hideChaff();\n clipboard.copy(common.getSelected() as ICopyable);\n return true;\n },\n keyCodes: [ctrlC, altC, metaC],\n };\n ShortcutRegistry.registry.register(copyShortcut);\n}\n\n/**\n * Keyboard shortcut to copy and delete a block on ctrl+x, cmd+x, or alt+x.\n *\n * @alias Blockly.ShortcutItems.registerCut\n */\nexport function registerCut() {\n const ctrlX = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.X, [KeyCodes.CTRL]);\n const altX =\n ShortcutRegistry.registry.createSerializedKey(KeyCodes.X, [KeyCodes.ALT]);\n const metaX = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.X, [KeyCodes.META]);\n\n const cutShortcut: KeyboardShortcut = {\n name: names.CUT,\n preconditionFn(workspace) {\n const selected = common.getSelected();\n return !workspace.options.readOnly && !Gesture.inProgress() &&\n selected != null && selected instanceof BlockSvg &&\n selected.isDeletable() && selected.isMovable() &&\n !selected.workspace!.isFlyout;\n },\n callback() {\n const selected = common.getSelected();\n if (!selected) {\n // Shouldn't happen but appeases the type system\n return false;\n }\n clipboard.copy(selected);\n (selected as BlockSvg).checkAndDelete();\n return true;\n },\n keyCodes: [ctrlX, altX, metaX],\n };\n\n ShortcutRegistry.registry.register(cutShortcut);\n}\n\n/**\n * Keyboard shortcut to paste a block on ctrl+v, cmd+v, or alt+v.\n *\n * @alias Blockly.ShortcutItems.registerPaste\n */\nexport function registerPaste() {\n const ctrlV = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.V, [KeyCodes.CTRL]);\n const altV =\n ShortcutRegistry.registry.createSerializedKey(KeyCodes.V, [KeyCodes.ALT]);\n const metaV = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.V, [KeyCodes.META]);\n\n const pasteShortcut: KeyboardShortcut = {\n name: names.PASTE,\n preconditionFn(workspace) {\n return !workspace.options.readOnly && !Gesture.inProgress();\n },\n callback() {\n return !!(clipboard.paste());\n },\n keyCodes: [ctrlV, altV, metaV],\n };\n\n ShortcutRegistry.registry.register(pasteShortcut);\n}\n\n/**\n * Keyboard shortcut to undo the previous action on ctrl+z, cmd+z, or alt+z.\n *\n * @alias Blockly.ShortcutItems.registerUndo\n */\nexport function registerUndo() {\n const ctrlZ = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.Z, [KeyCodes.CTRL]);\n const altZ =\n ShortcutRegistry.registry.createSerializedKey(KeyCodes.Z, [KeyCodes.ALT]);\n const metaZ = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.Z, [KeyCodes.META]);\n\n const undoShortcut: KeyboardShortcut = {\n name: names.UNDO,\n preconditionFn(workspace) {\n return !workspace.options.readOnly && !Gesture.inProgress();\n },\n callback(workspace) {\n // 'z' for undo 'Z' is for redo.\n (workspace as WorkspaceSvg).hideChaff();\n workspace.undo(false);\n return true;\n },\n keyCodes: [ctrlZ, altZ, metaZ],\n };\n ShortcutRegistry.registry.register(undoShortcut);\n}\n\n/**\n * Keyboard shortcut to redo the previous action on ctrl+shift+z, cmd+shift+z,\n * or alt+shift+z.\n *\n * @alias Blockly.ShortcutItems.registerRedo\n */\nexport function registerRedo() {\n const ctrlShiftZ = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.Z, [KeyCodes.SHIFT, KeyCodes.CTRL]);\n const altShiftZ = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.Z, [KeyCodes.SHIFT, KeyCodes.ALT]);\n const metaShiftZ = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.Z, [KeyCodes.SHIFT, KeyCodes.META]);\n // Ctrl-y is redo in Windows. Command-y is never valid on Macs.\n const ctrlY = ShortcutRegistry.registry.createSerializedKey(\n KeyCodes.Y, [KeyCodes.CTRL]);\n\n const redoShortcut: KeyboardShortcut = {\n name: names.REDO,\n preconditionFn(workspace) {\n return !Gesture.inProgress() && !workspace.options.readOnly;\n },\n callback(workspace) {\n // 'z' for undo 'Z' is for redo.\n (workspace as WorkspaceSvg).hideChaff();\n workspace.undo(true);\n return true;\n },\n keyCodes: [ctrlShiftZ, altShiftZ, metaShiftZ, ctrlY],\n };\n ShortcutRegistry.registry.register(redoShortcut);\n}\n\n/**\n * Registers all default keyboard shortcut item. This should be called once per\n * instance of KeyboardShortcutRegistry.\n *\n * @alias Blockly.ShortcutItems.registerDefaultShortcuts\n * @internal\n */\nexport function registerDefaultShortcuts() {\n registerEscape();\n registerDelete();\n registerCopy();\n registerCut();\n registerPaste();\n registerUndo();\n registerRedo();\n}\n\nregisterDefaultShortcuts();\n","/**\n * @license\n * Copyright 2012 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Utility functions for handling procedures.\n *\n * @namespace Blockly.Procedures\n */\nimport * as goog from '../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.Procedures');\n\n// Unused import preserved for side-effects. Remove if unneeded.\nimport './events/events_block_change.js';\n\nimport type {Block} from './block.js';\nimport type {BlockSvg} from './block_svg.js';\nimport {Blocks} from './blocks.js';\nimport * as common from './common.js';\nimport type {Abstract} from './events/events_abstract.js';\nimport type {BubbleOpen} from './events/events_bubble_open.js';\nimport * as eventUtils from './events/utils.js';\nimport type {Field} from './field.js';\nimport {Msg} from './msg.js';\nimport {Names} from './names.js';\nimport * as utilsXml from './utils/xml.js';\nimport * as Variables from './variables.js';\nimport type {Workspace} from './workspace.js';\nimport type {WorkspaceSvg} from './workspace_svg.js';\nimport * as Xml from './xml.js';\n\n\n/**\n * String for use in the \"custom\" attribute of a category in toolbox XML.\n * This string indicates that the category should be dynamically populated with\n * procedure blocks.\n * See also Blockly.Variables.CATEGORY_NAME and\n * Blockly.VariablesDynamic.CATEGORY_NAME.\n *\n * @alias Blockly.Procedures.CATEGORY_NAME\n */\nexport const CATEGORY_NAME = 'PROCEDURE';\n\n/**\n * The default argument for a procedures_mutatorarg block.\n *\n * @alias Blockly.Procedures.DEFAULT_ARG\n */\nexport const DEFAULT_ARG = 'x';\n\nexport type ProcedureTuple = [string, string[], boolean];\n\n/**\n * Procedure block type.\n *\n * @alias Blockly.Procedures.ProcedureBlock\n */\nexport interface ProcedureBlock {\n getProcedureCall: () => string;\n renameProcedure: (p1: string, p2: string) => void;\n getProcedureDef: () => ProcedureTuple;\n}\n\n/**\n * Find all user-created procedure definitions in a workspace.\n *\n * @param root Root workspace.\n * @returns Pair of arrays, the first contains procedures without return\n * variables, the second with. Each procedure is defined by a three-element\n * list of name, parameter list, and return value boolean.\n * @alias Blockly.Procedures.allProcedures\n */\nexport function allProcedures(root: Workspace):\n [ProcedureTuple[], ProcedureTuple[]] {\n const proceduresNoReturn =\n root.getBlocksByType('procedures_defnoreturn', false)\n .map(function(block) {\n return (block as unknown as ProcedureBlock).getProcedureDef();\n });\n const proceduresReturn =\n root.getBlocksByType('procedures_defreturn', false).map(function(block) {\n return (block as unknown as ProcedureBlock).getProcedureDef();\n });\n proceduresNoReturn.sort(procTupleComparator);\n proceduresReturn.sort(procTupleComparator);\n return [proceduresNoReturn, proceduresReturn];\n}\n\n/**\n * Comparison function for case-insensitive sorting of the first element of\n * a tuple.\n *\n * @param ta First tuple.\n * @param tb Second tuple.\n * @returns -1, 0, or 1 to signify greater than, equality, or less than.\n */\nfunction procTupleComparator(ta: ProcedureTuple, tb: ProcedureTuple): number {\n return ta[0].localeCompare(tb[0], undefined, {sensitivity: 'base'});\n}\n\n/**\n * Ensure two identically-named procedures don't exist.\n * Take the proposed procedure name, and return a legal name i.e. one that\n * is not empty and doesn't collide with other procedures.\n *\n * @param name Proposed procedure name.\n * @param block Block to disambiguate.\n * @returns Non-colliding name.\n * @alias Blockly.Procedures.findLegalName\n */\nexport function findLegalName(name: string, block: Block): string {\n if (block.isInFlyout) {\n // Flyouts can have multiple procedures called 'do something'.\n return name;\n }\n name = name || Msg['UNNAMED_KEY'] || 'unnamed';\n while (!isLegalName(name, block.workspace, block)) {\n // Collision with another procedure.\n const r = name.match(/^(.*?)(\\d+)$/);\n if (!r) {\n name += '2';\n } else {\n name = r[1] + (parseInt(r[2]) + 1);\n }\n }\n return name;\n}\n/**\n * Does this procedure have a legal name? Illegal names include names of\n * procedures already defined.\n *\n * @param name The questionable name.\n * @param workspace The workspace to scan for collisions.\n * @param opt_exclude Optional block to exclude from comparisons (one doesn't\n * want to collide with oneself).\n * @returns True if the name is legal.\n */\nfunction isLegalName(\n name: string, workspace: Workspace, opt_exclude?: Block): boolean {\n return !isNameUsed(name, workspace, opt_exclude);\n}\n\n/**\n * Return if the given name is already a procedure name.\n *\n * @param name The questionable name.\n * @param workspace The workspace to scan for collisions.\n * @param opt_exclude Optional block to exclude from comparisons (one doesn't\n * want to collide with oneself).\n * @returns True if the name is used, otherwise return false.\n * @alias Blockly.Procedures.isNameUsed\n */\nexport function isNameUsed(\n name: string, workspace: Workspace, opt_exclude?: Block): boolean {\n const blocks = workspace.getAllBlocks(false);\n // Iterate through every block and check the name.\n for (let i = 0; i < blocks.length; i++) {\n if (blocks[i] === opt_exclude) {\n continue;\n }\n // Assume it is a procedure block so we can check.\n const procedureBlock = blocks[i] as unknown as ProcedureBlock;\n if (procedureBlock.getProcedureDef) {\n const procName = procedureBlock.getProcedureDef();\n if (Names.equals(procName[0], name)) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Rename a procedure. Called by the editable field.\n *\n * @param name The proposed new name.\n * @returns The accepted name.\n * @alias Blockly.Procedures.rename\n */\nexport function rename(this: Field, name: string): string {\n // Strip leading and trailing whitespace. Beyond this, all names are legal.\n name = name.trim();\n\n const legalName = findLegalName(name, (this.getSourceBlock()));\n const oldName = this.getValue();\n if (oldName !== name && oldName !== legalName) {\n // Rename any callers.\n const blocks = this.getSourceBlock().workspace.getAllBlocks(false);\n for (let i = 0; i < blocks.length; i++) {\n // Assume it is a procedure so we can check.\n const procedureBlock = blocks[i] as unknown as ProcedureBlock;\n if (procedureBlock.renameProcedure) {\n procedureBlock.renameProcedure(oldName as string, legalName);\n }\n }\n }\n return legalName;\n}\n\n/**\n * Construct the blocks required by the flyout for the procedure category.\n *\n * @param workspace The workspace containing procedures.\n * @returns Array of XML block elements.\n * @alias Blockly.Procedures.flyoutCategory\n */\nexport function flyoutCategory(workspace: WorkspaceSvg): Element[] {\n const xmlList = [];\n if (Blocks['procedures_defnoreturn']) {\n // \n // do something\n // \n const block = utilsXml.createElement('block');\n block.setAttribute('type', 'procedures_defnoreturn');\n block.setAttribute('gap', '16');\n const nameField = utilsXml.createElement('field');\n nameField.setAttribute('name', 'NAME');\n nameField.appendChild(\n utilsXml.createTextNode(Msg['PROCEDURES_DEFNORETURN_PROCEDURE']));\n block.appendChild(nameField);\n xmlList.push(block);\n }\n if (Blocks['procedures_defreturn']) {\n // \n // do something\n // \n const block = utilsXml.createElement('block');\n block.setAttribute('type', 'procedures_defreturn');\n block.setAttribute('gap', '16');\n const nameField = utilsXml.createElement('field');\n nameField.setAttribute('name', 'NAME');\n nameField.appendChild(\n utilsXml.createTextNode(Msg['PROCEDURES_DEFRETURN_PROCEDURE']));\n block.appendChild(nameField);\n xmlList.push(block);\n }\n if (Blocks['procedures_ifreturn']) {\n // \n const block = utilsXml.createElement('block');\n block.setAttribute('type', 'procedures_ifreturn');\n block.setAttribute('gap', '16');\n xmlList.push(block);\n }\n if (xmlList.length) {\n // Add slightly larger gap between system blocks and user calls.\n xmlList[xmlList.length - 1].setAttribute('gap', '24');\n }\n\n /**\n * Add items to xmlList for each listed procedure.\n *\n * @param procedureList A list of procedures, each of which is defined by a\n * three-element list of name, parameter list, and return value boolean.\n * @param templateName The type of the block to generate.\n */\n function populateProcedures(\n procedureList: ProcedureTuple[], templateName: string) {\n for (let i = 0; i < procedureList.length; i++) {\n const name = procedureList[i][0];\n const args = procedureList[i][1];\n // \n // \n // \n // \n // \n const block = utilsXml.createElement('block');\n block.setAttribute('type', templateName);\n block.setAttribute('gap', '16');\n const mutation = utilsXml.createElement('mutation');\n mutation.setAttribute('name', name);\n block.appendChild(mutation);\n for (let j = 0; j < args.length; j++) {\n const arg = utilsXml.createElement('arg');\n arg.setAttribute('name', args[j]);\n mutation.appendChild(arg);\n }\n xmlList.push(block);\n }\n }\n\n const tuple = allProcedures(workspace);\n populateProcedures(tuple[0], 'procedures_callnoreturn');\n populateProcedures(tuple[1], 'procedures_callreturn');\n return xmlList;\n}\n\n/**\n * Updates the procedure mutator's flyout so that the arg block is not a\n * duplicate of another arg.\n *\n * @param workspace The procedure mutator's workspace. This workspace's flyout\n * is what is being updated.\n */\nfunction updateMutatorFlyout(workspace: WorkspaceSvg) {\n const usedNames = [];\n const blocks = workspace.getBlocksByType('procedures_mutatorarg', false);\n for (let i = 0, block; block = blocks[i]; i++) {\n usedNames.push(block.getFieldValue('NAME'));\n }\n\n const xmlElement = utilsXml.createElement('xml');\n const argBlock = utilsXml.createElement('block');\n argBlock.setAttribute('type', 'procedures_mutatorarg');\n const nameField = utilsXml.createElement('field');\n nameField.setAttribute('name', 'NAME');\n const argValue =\n Variables.generateUniqueNameFromOptions(DEFAULT_ARG, usedNames);\n const fieldContent = utilsXml.createTextNode(argValue);\n\n nameField.appendChild(fieldContent);\n argBlock.appendChild(nameField);\n xmlElement.appendChild(argBlock);\n\n workspace.updateToolbox(xmlElement);\n}\n\n/**\n * Listens for when a procedure mutator is opened. Then it triggers a flyout\n * update and adds a mutator change listener to the mutator workspace.\n *\n * @param e The event that triggered this listener.\n * @alias Blockly.Procedures.mutatorOpenListener\n * @internal\n */\nexport function mutatorOpenListener(e: Abstract) {\n if (e.type !== eventUtils.BUBBLE_OPEN) {\n return;\n }\n const bubbleEvent = e as BubbleOpen;\n if (!(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen) ||\n !bubbleEvent.blockId) {\n return;\n }\n const workspaceId = (bubbleEvent.workspaceId);\n const block = common.getWorkspaceById(workspaceId)!.getBlockById(\n bubbleEvent.blockId) as BlockSvg;\n const type = block.type;\n if (type !== 'procedures_defnoreturn' && type !== 'procedures_defreturn') {\n return;\n }\n const workspace = block.mutator!.getWorkspace() as WorkspaceSvg;\n updateMutatorFlyout(workspace);\n workspace.addChangeListener(mutatorChangeListener);\n}\n/**\n * Listens for changes in a procedure mutator and triggers flyout updates when\n * necessary.\n *\n * @param e The event that triggered this listener.\n */\nfunction mutatorChangeListener(e: Abstract) {\n if (e.type !== eventUtils.BLOCK_CREATE &&\n e.type !== eventUtils.BLOCK_DELETE &&\n e.type !== eventUtils.BLOCK_CHANGE) {\n return;\n }\n const workspaceId = e.workspaceId as string;\n const workspace = common.getWorkspaceById(workspaceId) as WorkspaceSvg;\n updateMutatorFlyout(workspace);\n}\n\n/**\n * Find all the callers of a named procedure.\n *\n * @param name Name of procedure.\n * @param workspace The workspace to find callers in.\n * @returns Array of caller blocks.\n * @alias Blockly.Procedures.getCallers\n */\nexport function getCallers(name: string, workspace: Workspace): Block[] {\n const callers = [];\n const blocks = workspace.getAllBlocks(false);\n // Iterate through every block and check the name.\n for (let i = 0; i < blocks.length; i++) {\n // Assume it is a procedure block so we can check.\n const procedureBlock = blocks[i] as unknown as ProcedureBlock;\n if (procedureBlock.getProcedureCall) {\n const procName = procedureBlock.getProcedureCall();\n // Procedure name may be null if the block is only half-built.\n if (procName && Names.equals(procName, name)) {\n callers.push(blocks[i]);\n }\n }\n }\n return callers;\n}\n\n/**\n * When a procedure definition changes its parameters, find and edit all its\n * callers.\n *\n * @param defBlock Procedure definition block.\n * @alias Blockly.Procedures.mutateCallers\n */\nexport function mutateCallers(defBlock: Block) {\n const oldRecordUndo = eventUtils.getRecordUndo();\n const procedureBlock = defBlock as unknown as ProcedureBlock;\n const name = procedureBlock.getProcedureDef()[0];\n const xmlElement = defBlock.mutationToDom!(true);\n const callers = getCallers(name, defBlock.workspace);\n for (let i = 0, caller; caller = callers[i]; i++) {\n const oldMutationDom = caller.mutationToDom!();\n const oldMutation = oldMutationDom && Xml.domToText(oldMutationDom);\n if (caller.domToMutation) {\n caller.domToMutation(xmlElement);\n }\n const newMutationDom = caller.mutationToDom!();\n const newMutation = newMutationDom && Xml.domToText(newMutationDom);\n if (oldMutation !== newMutation) {\n // Fire a mutation on every caller block. But don't record this as an\n // undo action since it is deterministically tied to the procedure's\n // definition mutation.\n eventUtils.setRecordUndo(false);\n eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(\n caller, 'mutation', null, oldMutation, newMutation));\n eventUtils.setRecordUndo(oldRecordUndo);\n }\n }\n}\n\n/**\n * Find the definition block for the named procedure.\n *\n * @param name Name of procedure.\n * @param workspace The workspace to search.\n * @returns The procedure definition block, or null not found.\n * @alias Blockly.Procedures.getDefinition\n */\nexport function getDefinition(name: string, workspace: Workspace): Block|null {\n // Do not assume procedure is a top block. Some languages allow nested\n // procedures. Also do not assume it is one of the built-in blocks. Only\n // rely on getProcedureDef.\n const blocks = workspace.getAllBlocks(false);\n for (let i = 0; i < blocks.length; i++) {\n // Assume it is a procedure block so we can check.\n const procedureBlock = blocks[i] as unknown as ProcedureBlock;\n if (procedureBlock.getProcedureDef) {\n const tuple = procedureBlock.getProcedureDef();\n if (tuple && Names.equals(tuple[0], name)) {\n return blocks[i]; // Can't use procedureBlock var due to type check.\n }\n }\n }\n return null;\n}\n","/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * An object that provides constants for rendering blocks.\n *\n * @class\n */\nimport * as goog from '../../../closure/goog/goog.js';\ngoog.declareModuleId('Blockly.blockRendering.ConstantProvider');\n\nimport {ConnectionType} from '../../connection_type.js';\nimport type {RenderedConnection} from '../../rendered_connection.js';\nimport type {BlockStyle, Theme} from '../../theme.js';\nimport * as colour from '../../utils/colour.js';\nimport * as dom from '../../utils/dom.js';\nimport * as parsing from '../../utils/parsing.js';\nimport {Svg} from '../../utils/svg.js';\nimport * as svgPaths from '../../utils/svg_paths.js';\n\n\n/** An object containing sizing and path information about outside corners. */\nexport interface OutsideCorners {\n topLeft: string;\n topRight: string;\n bottomRight: string;\n bottomLeft: string;\n rightHeight: number;\n}\n\n/** An object containing sizing and path information about inside corners. */\nexport interface InsideCorners {\n width: number;\n height: number;\n pathTop: string;\n pathBottom: string;\n}\n\n/** An object containing sizing and path information about a start hat. */\nexport interface StartHat {\n height: number;\n width: number;\n path: string;\n}\n\n/** An object containing sizing and path information about a notch. */\nexport interface Notch {\n type: number;\n width: number;\n height: number;\n pathLeft: string;\n pathRight: string;\n}\n\n/** An object containing sizing and path information about a puzzle tab. */\nexport interface PuzzleTab {\n type: number;\n width: number;\n height: number;\n pathDown: string|((p1: number) => string);\n pathUp: string|((p1: number) => string);\n}\n\n/**\n * An object containing sizing and path information about collapsed block\n * indicators.\n */\nexport interface JaggedTeeth {\n height: number;\n width: number;\n path: string;\n}\n\nexport type BaseShape = {\n type: number; width: number; height: number;\n};\n\n/** An object containing sizing and type information about a dynamic shape. */\nexport type DynamicShape = {\n type: number; width: (p1: number) => number; height: (p1: number) => number;\n isDynamic: true;\n connectionOffsetY: (p1: number) => number;\n connectionOffsetX: (p1: number) => number;\n pathDown: (p1: number) => string;\n pathUp: (p1: number) => string;\n pathRightDown: (p1: number) => string;\n pathRightUp: (p1: number) => string;\n};\n\n/** An object containing sizing and type information about a shape. */\nexport type Shape = BaseShape|DynamicShape;\n\n/**\n * Returns whether the shape is dynamic or not.\n *\n * @param shape The shape to check for dynamic-ness.\n * @returns Whether the shape is a dynamic shape or not.\n */\nexport function isDynamicShape(shape: Shape): shape is DynamicShape {\n return (shape as DynamicShape).isDynamic;\n}\n\n/**\n * An object that provides constants for rendering blocks.\n *\n * @alias Blockly.blockRendering.ConstantProvider\n */\nexport class ConstantProvider {\n /** The size of an empty spacer. */\n NO_PADDING = 0;\n\n /** The size of small padding. */\n SMALL_PADDING = 3;\n\n /** The size of medium padding. */\n MEDIUM_PADDING = 5;\n\n /** The size of medium-large padding. */\n MEDIUM_LARGE_PADDING = 8;\n\n /** The size of large padding. */\n LARGE_PADDING = 10;\n TALL_INPUT_FIELD_OFFSET_Y: number;\n\n /** The height of the puzzle tab used for input and output connections. */\n TAB_HEIGHT = 15;\n\n /**\n * The offset from the top of the block at which a puzzle tab is positioned.\n */\n TAB_OFFSET_FROM_TOP = 5;\n\n /**\n * Vertical overlap of the puzzle tab, used to make it look more like a\n * puzzle piece.\n */\n TAB_VERTICAL_OVERLAP = 2.5;\n\n /** The width of the puzzle tab used for input and output connections. */\n TAB_WIDTH = 8;\n\n /** The width of the notch used for previous and next connections. */\n NOTCH_WIDTH = 15;\n\n /** The height of the notch used for previous and next connections. */\n NOTCH_HEIGHT = 4;\n\n /** The minimum width of the block. */\n MIN_BLOCK_WIDTH = 12;\n EMPTY_BLOCK_SPACER_HEIGHT = 16;\n DUMMY_INPUT_MIN_HEIGHT: number;\n DUMMY_INPUT_SHADOW_MIN_HEIGHT: number;\n\n /** Rounded corner radius. */\n CORNER_RADIUS = 8;\n\n /**\n * Offset from the left side of a block or the inside of a statement input\n * to the left side of the notch.\n */\n NOTCH_OFFSET_LEFT = 15;\n STATEMENT_INPUT_NOTCH_OFFSET: number;\n\n STATEMENT_BOTTOM_SPACER = 0;\n STATEMENT_INPUT_PADDING_LEFT = 20;\n\n /** Vertical padding between consecutive statement inputs. */\n BETWEEN_STATEMENT_PADDING_Y = 4;\n TOP_ROW_MIN_HEIGHT: number;\n TOP_ROW_PRECEDES_STATEMENT_MIN_HEIGHT: number;\n BOTTOM_ROW_MIN_HEIGHT: number;\n BOTTOM_ROW_AFTER_STATEMENT_MIN_HEIGHT: number;\n\n /**\n * Whether to add a 'hat' on top of all blocks with no previous or output\n * connections. Can be overridden by 'hat' property on Theme.BlockStyle.\n */\n ADD_START_HATS = false;\n\n /** Height of the top hat. */\n START_HAT_HEIGHT = 15;\n\n /** Width of the top hat. */\n START_HAT_WIDTH = 100;\n\n SPACER_DEFAULT_HEIGHT = 15;\n\n MIN_BLOCK_HEIGHT = 24;\n\n EMPTY_INLINE_INPUT_PADDING = 14.5;\n EMPTY_INLINE_INPUT_HEIGHT: number;\n\n EXTERNAL_VALUE_INPUT_PADDING = 2;\n EMPTY_STATEMENT_INPUT_HEIGHT: number;\n START_POINT: string;\n\n /** Height of SVG path for jagged teeth at the end of collapsed blocks. */\n JAGGED_TEETH_HEIGHT = 12;\n\n /** Width of SVG path for jagged teeth at the end of collapsed blocks. */\n JAGGED_TEETH_WIDTH = 6;\n\n /** Point size of text. */\n FIELD_TEXT_FONTSIZE = 11;\n\n /** Text font weight. */\n FIELD_TEXT_FONTWEIGHT = 'normal';\n\n /** Text font family. */\n FIELD_TEXT_FONTFAMILY = 'sans-serif';\n\n /**\n * Height of text. This constant is dynamically set in\n * `setFontConstants_` to be the height of the text based on the font\n * used.\n */\n FIELD_TEXT_HEIGHT = -1; // Dynamically set.\n\n /**\n * Text baseline. This constant is dynamically set in `setFontConstants_`\n * to be the baseline of the text based on the font used.\n */\n FIELD_TEXT_BASELINE = -1; // Dynamically set.\n\n /** A field's border rect corner radius. */\n FIELD_BORDER_RECT_RADIUS = 4;\n\n /** A field's border rect default height. */\n FIELD_BORDER_RECT_HEIGHT = 16;\n\n /** A field's border rect X padding. */\n FIELD_BORDER_RECT_X_PADDING = 5;\n\n /** A field's border rect Y padding. */\n FIELD_BORDER_RECT_Y_PADDING = 3;\n\n /**\n * The backing colour of a field's border rect.\n *\n * @internal\n */\n FIELD_BORDER_RECT_COLOUR = '#fff';\n FIELD_TEXT_BASELINE_CENTER: boolean;\n FIELD_DROPDOWN_BORDER_RECT_HEIGHT: number;\n\n /**\n * Whether or not a dropdown field should add a border rect when in a shadow\n * block.\n */\n FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW = false;\n\n /**\n * Whether or not a dropdown field's div should be coloured to match the\n * block colours.\n */\n FIELD_DROPDOWN_COLOURED_DIV = false;\n\n /** Whether or not a dropdown field uses a text or SVG arrow. */\n FIELD_DROPDOWN_SVG_ARROW = false;\n FIELD_DROPDOWN_SVG_ARROW_PADDING: number;\n\n /** A dropdown field's SVG arrow size. */\n FIELD_DROPDOWN_SVG_ARROW_SIZE = 12;\n FIELD_DROPDOWN_SVG_ARROW_DATAURI: string;\n\n /**\n * Whether or not to show a box shadow around the widget div. This is only a\n * feature of full block fields.\n */\n FIELD_TEXTINPUT_BOX_SHADOW = false;\n\n /**\n * Whether or not the colour field should display its colour value on the\n * entire block.\n */\n FIELD_COLOUR_FULL_BLOCK = false;\n\n /** A colour field's default width. */\n FIELD_COLOUR_DEFAULT_WIDTH = 26;\n FIELD_COLOUR_DEFAULT_HEIGHT: number;\n FIELD_CHECKBOX_X_OFFSET: number;\n /** @internal */\n randomIdentifier: string;\n\n /**\n * The defs tag that contains all filters and patterns for this Blockly\n * instance.\n */\n private defs_: SVGElement|null = null;\n\n /**\n * The ID of the emboss filter, or the empty string if no filter is set.\n *\n * @internal\n */\n embossFilterId = '';\n\n /** The element to use for highlighting, or null if not set. */\n private embossFilter_: SVGElement|null = null;\n\n /**\n * The ID of the disabled pattern, or the empty string if no pattern is set.\n *\n * @internal\n */\n disabledPatternId = '';\n\n /**\n * The element to use for disabled blocks, or null if not set.\n */\n private disabledPattern_: SVGElement|null = null;\n\n /**\n * The ID of the debug filter, or the empty string if no pattern is set.\n */\n debugFilterId = '';\n\n /**\n * The element to use for a debug highlight, or null if not set.\n */\n private debugFilter_: SVGElement|null = null;\n\n /** The