App Inventer 2 block helper

An easy way to operate blocks at MIT App Inventor 2.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         App Inventer 2 block helper
// @version      0.7.8
// @namespace    App Inventer 2 block helper
// @description  An easy way to operate blocks at MIT App Inventor 2.
// @author       [email protected]
// @match        https://*.appinventor.mit.edu/*
// @match        http://localhost/*
// @match        http://192.168.*.*/*
// @license      MIT

// ==/UserScript==

(function() {
    //'use strict';

    setTimeout(() => {
		var lastBlock;
		var types=["component_event","global_declaration","procedures_defreturn","procedures_defnoreturn"];

		function genHelperPalette(){

			var box = document.createElement('div');
			document.querySelector(".ode-WorkColumns").appendChild(box);

			box.outerHTML='<div class="ode-Box" aria-hidden="true" style="width: 220px; display: none;" id="helperPalette"><div class="ode-Box-content" ><table cellspacing="0" cellpadding="0" class="ode-Box-header" style="width: 100%;"><tbody><tr><td align="left" width="" height="" rowspan="1" style="vertical-align: top;"><div style="width: 100%;"><div class="ode-Box-header-caption" style="white-space: nowrap;">AI2HELPER </div></div></td></tr></tbody></table><div style="margin:3px"><input type="text" id="keyword" ></div><div tabindex="0" class="ode-TextButton" id="searchkeyword" style="margin:3px">search keyword</div><div tabindex="0" class="ode-TextButton" id="removeallcomments" style="margin:3px">remove all comments</div><div tabindex="0" class="ode-TextButton" id="downloadPNGIgnoreOrphan"  style="margin:3px">download all as png</div><div tabindex="0" class="ode-TextButton" id="updateoutline"  style="margin:3px">update outline</div><div   id="helperOutline"></div></div></div>';
		}

        var isHelperOpen = false;
		function switchHelper(){
			var helper = document.querySelector("#helperPalette");
			if(helper.style.display == "none"){
				helper.style.display = "block"
				updateoutline();
                isHelperOpen = true;
			}else{
				helper.style.display = "none"
                 isHelperOpen = false;
			}
		}


		function updateoutline(){

			let blocks = Blockly.getMainWorkspace().getTopBlocks().filter((block) => types.indexOf(block.type)>-1);
			blocks.sort((a,b) => a.toString().localeCompare(b.toString()));

			var helperOutline = document.querySelector("#helperOutline");
			helperOutline.innerHTML="";
			blocks.forEach((block) =>{
					let div = document.createElement('DIV');
					helperOutline.appendChild(div);
					//div.outerHTML='<div class="gwt-TreeItem" style="white-space: nowrap; padding:3px; overflow:hidden;" id="id'+block.id+'" onclick="btnonclick(this);">'+ simpleString(block)+'</div>';
					//div.className = "gwt-TreeItem";
					div.style = "white-space: nowrap; padding:3px; overflow:hidden;";
					div.id = "id"+block.id;
					div.addEventListener("click",() => {
						btnonclick(div);
					})
					div.innerHTML =  simpleString(block);

				})
		}


		function btnonclick(obj){
            if(lastBlock) {lastBlock.setHighlighted(false);}
            var block =Blockly.getMainWorkspace().getBlockById(obj.id.substr(2));
            if(block == lastBlock){
                lastBlock = null;
            }else{
                Blockly.getMainWorkspace().centerOnBlock(block.id);
                block.select();
                block.setHighlighted(true);
                lastBlock = block;
            }
		}


		function simpleString(block) {
				var text = '';
				for (var i = 0, input;
					 (input = block.inputList[i]); i++) {
					if (input.name == Blockly.BlockSvg.COLLAPSED_INPUT_NAME) {
						continue;
					}
					for (var j = 0, field;
						 (field = input.fieldRow[j]); j++) {
						text += field.getText() + ' ';
					}
				}
				text = goog.string.trim(text) || '???';
				return text;
			}

		function removeallcomments(){
			if(confirm("Are you sure to remove all comments?")){
				Blockly.getMainWorkspace().getAllBlocks().forEach(b=>{b.setCommentText(null)});
			}

		}

		function searchkeyword(){
			var input = document.querySelector("#keyword");
			if(input.value){
				findBlock(input.value);
			}
		}

		var lastIndex = -1;

		function findBlock(keyword){
			var blocks = Blockly.getMainWorkspace().getAllBlocks().filter(block=>simpleString(block).toLowerCase().includes(keyword.toLowerCase()));
			if(lastIndex > -1){
				blocks[lastIndex].setHighlighted(false);
			}
			lastIndex++;
			if(lastIndex<blocks.length){
				expand(blocks[lastIndex]);
				Blockly.getMainWorkspace().cleanUp()
				Blockly.getMainWorkspace().centerOnBlock(blocks[lastIndex].id);
				blocks[lastIndex].select();
				blocks[lastIndex].setHighlighted(true);
			}else{
				lastIndex = -1;
			}
		}

		function expand(block){
			block.setCollapsed(false);
			let parent = block.getParent();
			if(parent){
				expand(parent);
			}
		}

		function downloadPNGIgnoreOrphan(){
			var topblocks=Blockly.getMainWorkspace().getTopBlocks();
			var blocks=topblocks.filter((block)=>{
				return types.indexOf(block.type)>=0
			});
			if(confirm("Are you sure to download all " + blocks.length + " blocks?")){

				var i=0;
				var timer=setTimeout(function(){
					if(i<blocks.length){
						exportBlockAsPng(blocks[i]);
						i++;
						timer=setTimeout(arguments.callee,1000)
					}
				},1000);
			}
		}

		function genHelperButton(){
			let btnHelper = document.createElement('div');
            let allRight = document.querySelectorAll(".right");
			var container = allRight[allRight.length-1];

			container.appendChild(btnHelper);
			btnHelper.outerHTML='<div tabindex="0" class="ode-TextButton" id="helperButton">AI2HELPER</div>';


		}



		genHelperButton();
		genHelperPalette();
		document.querySelector("#removeallcomments").addEventListener("click",() => {
			removeallcomments();
		})
		document.querySelector("#helperButton").addEventListener("click",() => {
			switchHelper();
		})


		document.querySelector("#searchkeyword").addEventListener("click",() => {
			searchkeyword();
		})
		document.querySelector("#downloadPNGIgnoreOrphan").addEventListener("click",() => {
			downloadPNGIgnoreOrphan();
		})
		document.querySelector("#updateoutline").addEventListener("click",() => {
			updateoutline();
		})


        //set designer panel scroll seperately
        //document.querySelector(".ode-ProjectListView").childNodes[1].style.overflow = "auto";
        //document.querySelector(".ode-ProjectListView").childNodes[1].style.height="100%";
        //document.querySelector(".ode-TutorialWrapper").style.overflowY="hidden";
        //document.querySelector(".ode-WorkColumns").childNodes.forEach(node => {node.style.height = "calc(100% - 38px)"; node.style.overflow = "auto"});

		///////////////////////////////////////////////////////////////
		///////below codes from MIT App Inventer source code///////////
		///////////////////////////////////////////////////////////////

		var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';

			function isExternal(url) {
				return url && url.lastIndexOf('http', 0) == 0 && url.lastIndexOf(window.location.host) == -1
			}

			function styles(el, selectorRemap) {
				var css = "";
				var sheets = document.styleSheets;
				for (var i = 0; i < sheets.length; i++) {
					if (isExternal(sheets[i].href)) {
						console.warn("Cannot include styles from other hosts: " + sheets[i].href);
						continue
					}
					var rules = null;
					try {
						rules = sheets[i].cssRules
					} catch (e) {
						console.warn('Skipping a potentially injected stylesheet', e);
						continue
					}
					if (rules != null) {
						for (var j = 0; j < rules.length; j++) {
							var rule = rules[j];
							if (typeof(rule.style) != "undefined") {
								var match = null;
								try {
									match = el.querySelector(rule.selectorText)
								} catch (err) {
									console.warn('Invalid CSS selector "' + rule.selectorText + '"', err)
								}
								if (match && rule.selectorText.indexOf("blocklySelected") == -1) {
									var selector = selectorRemap ? selectorRemap(rule.selectorText) : rule.selectorText;
									css += selector + " { " + rule.style.cssText + " }"
								} else if (rule.cssText.match(/^@font-face/)) {
									css += rule.cssText + ''
								}
							}
						}
					}
				}
				return css
			}
			function svgAsDataUri(el, optmetrics, options, cb) {
				options = options || {};
				options.scale = options.scale || 1;
				var xmlns = "http://www.w3.org/2000/xmlns/";
				var outer = document.createElement("div");
				var textAreas = document.getElementsByTagName("textarea");
				for (var i = 0; i < textAreas.length; i++) {
					textAreas[i].innerHTML = textAreas[i].value
				}
				var clone = el.cloneNode(true);
				var width, height;
				if (el.tagName == 'svg') {
					var box = el.getBoundingClientRect();
					width = box.width || parseInt(clone.getAttribute('width') || clone.style.width || window.getComputedStyle(el).getPropertyValue('width'));
					height = box.height || parseInt(clone.getAttribute('height') || clone.style.height || window.getComputedStyle(el).getPropertyValue('height'));
					var left = (parseFloat(optmetrics.contentLeft) - parseFloat(optmetrics.viewLeft)).toString();
					var top = (parseFloat(optmetrics.contentTop) - parseFloat(optmetrics.viewTop)).toString();
					var right = (parseFloat(optmetrics.contentWidth)).toString();
					var bottom = (parseFloat(optmetrics.contentHeight)).toString();
					clone.setAttribute("viewBox", left + " " + top + " " + right + " " + bottom)
				} else {
					var matrix = el.getScreenCTM();
					//clone.setAttribute('transform', clone.getAttribute('transform').replace(/translate\(.*?\)/, '').replace(/scale\(.*?\)/, '').trim());
					clone.setAttribute('transform', "");
					var box = el.getBBox();
					width = box.width;
					height = box.height;
					var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
					svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
					svg.appendChild(clone);
					clone = svg;
					clone.setAttribute('viewBox', box.x + " " + box.y + " " + width + " " + height)
				}
				clone.setAttribute("version", "1.1");
				clone.setAttribute("width", width);
				clone.setAttribute("height", height);
				clone.setAttribute("style", 'background-color: rgba(255, 255, 255, 0);');
				outer.appendChild(clone);
				var css = styles(el, options.selectorRemap);
				var s = document.createElement('style');
				s.setAttribute('type', 'text/css');
				s.innerHTML = "<![CDATA[" + css + "]]>";
				var defs = document.createElement('defs');
				defs.appendChild(s);
				clone.insertBefore(defs, clone.firstChild);
				var toHide = clone.getElementsByClassName("blocklyScrollbarHandle");
				for (var i = 0; i < toHide.length; i++) {
					toHide[i].setAttribute("visibility", "hidden")
				}
				toHide = clone.getElementsByClassName("blocklyScrollbarBackground");
				for (var i = 0; i < toHide.length; i++) {
					toHide[i].setAttribute("visibility", "hidden")
				}
				toHide = clone.querySelectorAll('image');
				for (var i = 0; i < toHide.length; i++) {
					toHide[i].setAttribute("visibility", "hidden")
				}
				toHide = clone.querySelectorAll('.blocklyMainBackground');
				for (var i = 0; i < toHide.length; i++) {
					toHide[i].parentElement.removeChild(toHide[i])
				}
				var zelement = clone.getElementById("rectCorner");
				if (zelement) {
					zelement.setAttribute("visibility", "hidden")
				}
				zelement = clone.getElementById("indicatorWarning");
				if (zelement) {
					zelement.setAttribute("visibility", "hidden")
				}
				var svg = doctype + outer.innerHTML;
				svg = svg.replace(/&nbsp/g, '&#160');
				svg = svg.replace(/sans-serif/g, 'Arial, Verdana, "Nimbus Sans L", Helvetica');
				var uri = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svg)));
				if (cb) {
					cb(uri)
				}
			}

			function makeCRCTable() {
				var c;
				var crcTable = [];
				for (var n = 0; n < 256; n++) {
					c = n;
					for (var k = 0; k < 8; k++) {
						c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1))
					}
					crcTable[n] = c
				}
				return crcTable
			}

			function crc32(data) {
				var crcTable = window.crcTable || (window.crcTable = makeCRCTable());
				var crc = 0 ^ (-1);
				for (var i = 0; i < data.length; i++) {
					crc = (crc >>> 8) ^ crcTable[(crc ^ data[i]) & 0xFF]
				}
				return (crc ^ (-1)) >>> 0
			}
			var CODE_PNG_CHUNK = 'coDe';

			function PNG() {
				this.chunks = null
			}
			PNG.HEADER = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
			var pHY_data = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
			PNG.Chunk = function(length, type, data, crc) {
				this.length = length;
				this.type = type;
				this.data = data;
				this.crc = crc
			};
			PNG.prototype.readFromBlob = function(blob, callback) {
				var reader = new FileReader();
				var png = this;
				reader.addEventListener('loadend', function() {
					png.processData_(new Uint8Array(reader.result));
					if (callback instanceof Function) callback(png)
				});
				reader.readAsArrayBuffer(blob)
			};
			PNG.prototype.getCodeChunk = function() {
				if (!this.chunks) return null;
				for (var i = 0; i < this.chunks.length; i++) {
					if (this.chunks[i].type === CODE_PNG_CHUNK) {
						return this.chunks[i]
					}
				}
				return null
			};
			PNG.prototype.processData_ = function(data) {
				var chunkStart = PNG.HEADER.length;

				function decode4() {
					var num;
					num = data[chunkStart++];
					num = num * 256 + data[chunkStart++];
					num = num * 256 + data[chunkStart++];
					num = num * 256 + data[chunkStart++];
					return num
				}

				function read4() {
					var str = '';
					for (var i = 0; i < 4; i++, chunkStart++) {
						str += String.fromCharCode(data[chunkStart])
					}
					return str
				}

				function readData(length) {
					return data.slice(chunkStart, chunkStart + length)
				}
				this.chunks = [];
				while (chunkStart < data.length) {
					var length = decode4();
					var type = read4();
					var chunkData = readData(length);
					chunkStart += length;
					var crc = decode4();
					this.chunks.push(new PNG.Chunk(length, type, chunkData, crc))
				}
			};
			PNG.prototype.setCodeChunk = function(code) {
				var text = new TextEncoder().encode(CODE_PNG_CHUNK + code);
				var length = text.length - 4;
				var crc = crc32(text);
				text = text.slice(4);
				for (var i = 0, chunk;
					 (chunk = this.chunks[i]); i++) {
					if (chunk.type === CODE_PNG_CHUNK) {
						chunk.length = length;
						chunk.data = text;
						chunk.crc = crc;
						return
					}
				}
				chunk = new PNG.Chunk(length, CODE_PNG_CHUNK, text, crc);
				this.chunks.splice(this.chunks.length - 1, 0, chunk)
			};
			PNG.prototype.toBlob = function() {
				var length = PNG.HEADER.length;
				this.chunks.forEach(function(chunk) {
					length += chunk.length + 12
				});
				var buffer = new Uint8Array(length);
				var index = 0;

				function write4(value) {
					if (typeof value === 'string') {
						var text = new TextEncoder().encode(value);
						buffer.set(text, index);
						index += text.length
					} else {
						buffer[index + 3] = value & 0xFF;
						value >>= 8;
						buffer[index + 2] = value & 0xFF;
						value >>= 8;
						buffer[index + 1] = value & 0xFF;
						value >>= 8;
						buffer[index] = value & 0xFF;
						index += 4
					}
				}

				function writeData(data) {
					buffer.set(data, index);
					index += data.length
				}
				writeData(PNG.HEADER);
				this.chunks.forEach(function(chunk) {
					write4(chunk.length);
					write4(chunk.type);
					writeData(chunk.data);
					write4(chunk.crc)
				});
				return new Blob([buffer], {
					'type': 'image/png'
				})
			};
			function exportBlockAsPng(block) {
				var xml = document.createElement('xml');
				xml.appendChild(Blockly.Xml.blockToDom(block, true));
				var code = Blockly.Xml.domToText(xml);
				svgAsDataUri(block.svgGroup_, block.workspace.getMetrics(), null, function(uri) {
					var img = new Image();
					img.src = uri;
					img.onload = function() {
						var canvas = document.createElement('canvas');
						canvas.width = 2 * img.width;
						canvas.height = 2 * img.height;
						var context = canvas.getContext('2d');
						context.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);

						function download(png) {
							png.setCodeChunk(code);
							for (var i = 0; i < png.chunks.length; i++) {
								var phy = [112, 72, 89, 115];
								if (png.chunks[i].type == 'pHYs') {
									png.chunks.splice(i, 1, new PNG.Chunk(9, 'pHYs', pHY_data, crc32(phy.concat(pHY_data))));
									break
								} else if (png.chunks[i].type == 'IDAT') {
									png.chunks.splice(i, 0, new PNG.Chunk(9, 'pHYs', pHY_data, crc32(phy.concat(pHY_data))));
									break
								}
							}
							var blob = png.toBlob();
							var a = document.createElement('a');
							a.download = simpleString(block) + '.png';
							a.target = '_self';
							a.href = URL.createObjectURL(blob);
							document.body.appendChild(a);
							a.addEventListener("click", function(e) {
								a.parentNode.removeChild(a)
							});
							a.click()
						}
						if (canvas.toBlob === undefined) {
							var src = canvas.toDataURL('image/png');
							var base64img = src.split(',')[1];
							var decoded = window.atob(base64img);
							var rawLength = decoded.length;
							var buffer = new Uint8Array(new ArrayBuffer(rawLength));
							for (var i = 0; i < rawLength; i++) {
								buffer[i] = decoded.charCodeAt(i)
							}
							var blob = new Blob([buffer], {
								'type': 'image/png'
							});
							new PNG().readFromBlob(blob, download)
						} else {
							canvas.toBlob(function(blob) {
								new PNG().readFromBlob(blob, download)
							})
						}
					}
				})
			};

    }, 20000);

})();