From b4159b986715a75f748c49dd60bb9f812b435e7e Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Sun, 18 Nov 2018 17:03:21 -0800 Subject: [PATCH 01/11] Added Max Instances property to Blocks --- core/block.js | 64 +++++++++++++++++++++++++++++++++++++++++++++ core/blockly.js | 11 +++++++- core/contextmenu.js | 6 +---- core/flyout_base.js | 5 +++- core/workspace.js | 53 +++++++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 7 deletions(-) diff --git a/core/block.js b/core/block.js index eef55c47a09..527065692a4 100644 --- a/core/block.js +++ b/core/block.js @@ -67,6 +67,8 @@ Blockly.Block = function(workspace, prototypeName, opt_id) { workspace.blockDB_[this.id] = this; /** @type {Blockly.Connection} */ this.outputConnection = null; + /** @type {number} */ + this.maxInstances = Infinity; /** @type {Blockly.Connection} */ this.nextConnection = null; /** @type {Blockly.Connection} */ @@ -164,6 +166,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) { } workspace.addTopBlock(this); + workspace.addTypedBlock(this); // Call an initialization function, if it exists. if (typeof this.init == 'function') { @@ -255,6 +258,7 @@ Blockly.Block.prototype.dispose = function(healStack) { // Remove this block from the workspace's list of top-most blocks. if (this.workspace) { this.workspace.removeTopBlock(this); + this.workspace.removeTypedBlock(this); // Remove from block database. delete this.workspace.blockDB_[this.id]; this.workspace = null; @@ -668,6 +672,53 @@ Blockly.Block.prototype.setMovable = function(movable) { this.movable_ = movable; }; +/** + * Get whether is block is duplicatable or not. If any decendent is not + * duplicatable this block is not duplicatable. If duplicating this block and + * decendents will put this block over the workspace\'s capacity this block is + * not duplicatable. If duplicating this block and decendents will put any + * decendent over their maxInstances this block is not duplicatable. + * @return {boolean} True if duplicatable + */ +Blockly.Block.prototype.isDuplicatable = function() { + var instancesOfType = this.workspace.getBlocksByType(this.type); + if(instancesOfType.length >= this.maxInstances) { + return false; + } + + var copyableBlocks = this.getDescendants(true); + // Remove all "next statement" blocks because they will not be copied. + if (this.getNextBlock()){ + var index = copyableBlocks.indexOf(this.getNextBlock()); + copyableBlocks.splice(index, copyableBlocks.length - index); + } + + if(copyableBlocks.length > this.workspace.remainingCapacity()) { + return false; + } + + // If we do not have any blocks to copy besides ourself, we can just + // return true, because we already checked ourself. + if (copyableBlocks.length > 1) { + if (copyableBlocks.filter(a => a.type == this.type).length + + instancesOfType.length > this.maxInstances) { + return false; + } + + var children = this.getChildren(false); + for (var i = 0; i < children.length; i++) { + // No need to check the next block because it will not be duplicated + if (children[i] === this.getNextBlock()) { + continue; + } + if (!children[i].isDuplicatable()) { + return false; + } + } + } + return true; +} + /** * Get whether this block is a shadow block or not. * @return {boolean} True if a shadow. @@ -1071,6 +1122,16 @@ Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { } }; +/** + * Set the maximum number of instances of this block allowed at a time. + * @param {number} maxInstances The maximum number of instances. + */ +Blockly.Block.prototype.setMaxInstances = function(maxInstances) { + if (maxInstances) { + this.maxInstances = maxInstances; + } +}; + /** * Set whether value inputs are arranged horizontally or vertically. * @param {boolean} newBoolean True if inputs are horizontal. @@ -1264,6 +1325,9 @@ Blockly.Block.prototype.jsonInit = function(json) { if (json['output'] !== undefined) { this.setOutput(true, json['output']); } + if (json['maxInstances'] !== undefined) { + this.setMaxInstances(json['maxInstances']); + } if (json['previousStatement'] !== undefined) { this.setPreviousStatement(true, json['previousStatement']); } diff --git a/core/blockly.js b/core/blockly.js index bfd58cdff3c..bda6daa07c8 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -98,6 +98,13 @@ Blockly.clipboardXml_ = null; */ Blockly.clipboardSource_ = null; +/** + * Copied object. + * @type {Blockly.Block | Blockly.WorkspaceComment} + * @private + */ +Blockly.copiedObject_ = null; + /** * Cached value for whether 3D is supported. * @type {!boolean} @@ -226,7 +233,8 @@ Blockly.onKeyDown_ = function(e) { } if (e.keyCode == 86) { // 'v' for paste. - if (Blockly.clipboardXml_) { + if (Blockly.clipboardXml_ && (Blockly.copiedObject_.isComment || + Blockly.copiedObject_.isDuplicatable())) { Blockly.Events.setGroup(true); // Pasting always pastes to the main workspace, even if the copy // started in a flyout workspace. @@ -273,6 +281,7 @@ Blockly.copy_ = function(toCopy) { } Blockly.clipboardXml_ = xml; Blockly.clipboardSource_ = toCopy.workspace; + Blockly.copiedObject_ = toCopy; }; /** diff --git a/core/contextmenu.js b/core/contextmenu.js index b23e8e11ea5..bd8b38f8e06 100644 --- a/core/contextmenu.js +++ b/core/contextmenu.js @@ -260,11 +260,7 @@ Blockly.ContextMenu.blockHelpOption = function(block) { * @package */ Blockly.ContextMenu.blockDuplicateOption = function(block) { - var enabled = true; - if (block.getDescendants(false).length > - block.workspace.remainingCapacity()) { - enabled = false; - } + var enabled = block.isDuplicatable(); var duplicateOption = { text: Blockly.Msg['DUPLICATE_BLOCK'], enabled: enabled, diff --git a/core/flyout_base.js b/core/flyout_base.js index 235a03bb981..33bc005d6c6 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -228,6 +228,7 @@ Blockly.Flyout.prototype.createDom = function(tagName) { Blockly.Flyout.prototype.init = function(targetWorkspace) { this.targetWorkspace_ = targetWorkspace; this.workspace_.targetWorkspace = targetWorkspace; + // Add scrollbar. this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, this.horizontalLayout_, false, 'blocklyFlyoutScrollbar'); @@ -729,7 +730,9 @@ Blockly.Flyout.prototype.filterForCapacity_ = function() { for (var i = 0, block; block = blocks[i]; i++) { if (this.permanentlyDisabled_.indexOf(block) == -1) { var allBlocks = block.getDescendants(false); - block.setDisabled(allBlocks.length > remainingCapacity); + block.setDisabled(allBlocks.length > remainingCapacity || + this.targetWorkspace_.getBlocksByType(block.type).length >= + block.maxInstances); } } }; diff --git a/core/workspace.js b/core/workspace.js index 6040f7da440..0a3120f7d0a 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -85,6 +85,11 @@ Blockly.Workspace = function(opt_options) { * @private */ this.blockDB_ = Object.create(null); + /** + * @type {!Object} + * @private + */ + this.typedBlocksDB_ = Object.create(null); /** * A map from variable type to list of variable names. The lists contain all @@ -189,6 +194,54 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) { return blocks; }; +/** Add a block to the list of blocks keyed by type. + * @param {!Blockly.Block} block Block to add. + */ +Blockly.Workspace.prototype.addTypedBlock = function(block) { + if(!this.typedBlocksDB_[block.type]) { + this.typedBlocksDB_[block.type] = []; + } + this.typedBlocksDB_[block.type].push(block); +} + +/** Remove a block from the list of blocks keyed by type. + * @param {!Blockly.Block} block Block to remove. + */ +Blockly.Workspace.prototype.removeTypedBlock = function(block) { + this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type].indexOf(block), 1); + if (this.typedBlocksDB_[block.type].length === 0) { + delete this.typedBlocksDB_[block.type]; + } +} + +/** + * Finds the blocks with the associated type and returns them. Blocks are + * optionally sorted by position; top to bottom (with slight LTR or RTL bias). + * @param {string} type The type of block to search for. + * @param {boolean} ordered Sor the list if true. + * @return {!Array.} The blocks of the given type. + */ +Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { + if (this.typedBlocksDB_[type]) { + var blocks = [].concat(this.typedBlocksDB_[type]); + } else { + return []; + } + if (ordered && blocks.length > 1) { + var offset = + Math.sign(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); + if (this.RTL) { + offset *= -1; + } + blocks.sort(function(a, b) { + var aXY = a.getRelativeToSurfaceXY(); + var bXY = b.getRelativeToSurfaceXY(); + return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); + }); + } + return blocks; +} + /** * Add a comment to the list of top comments. * @param {!Blockly.WorkspaceComment} comment comment to add. From a723541ae2040cd56b29cbcff5d244aea0cfb7b3 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 20 Nov 2018 13:17:14 -0800 Subject: [PATCH 02/11] eslint cleanup --- core/block.js | 10 +++++++--- core/workspace.js | 14 +++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/block.js b/core/block.js index 527065692a4..af37af82490 100644 --- a/core/block.js +++ b/core/block.js @@ -688,7 +688,7 @@ Blockly.Block.prototype.isDuplicatable = function() { var copyableBlocks = this.getDescendants(true); // Remove all "next statement" blocks because they will not be copied. - if (this.getNextBlock()){ + if (this.getNextBlock()) { var index = copyableBlocks.indexOf(this.getNextBlock()); copyableBlocks.splice(index, copyableBlocks.length - index); } @@ -700,8 +700,12 @@ Blockly.Block.prototype.isDuplicatable = function() { // If we do not have any blocks to copy besides ourself, we can just // return true, because we already checked ourself. if (copyableBlocks.length > 1) { - if (copyableBlocks.filter(a => a.type == this.type).length + - instancesOfType.length > this.maxInstances) { + var type = this.type; + var copyableBlocksOfType = copyableBlocks.filter(function(block) { + return block.type == type; + }); + if (copyableBlocksOfType.length + instancesOfType.length > + this.maxInstances) { return false; } diff --git a/core/workspace.js b/core/workspace.js index 0a3120f7d0a..124a2ae940b 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -198,11 +198,11 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) { * @param {!Blockly.Block} block Block to add. */ Blockly.Workspace.prototype.addTypedBlock = function(block) { - if(!this.typedBlocksDB_[block.type]) { + if (!this.typedBlocksDB_[block.type]) { this.typedBlocksDB_[block.type] = []; } this.typedBlocksDB_[block.type].push(block); -} +}; /** Remove a block from the list of blocks keyed by type. * @param {!Blockly.Block} block Block to remove. @@ -212,10 +212,10 @@ Blockly.Workspace.prototype.removeTypedBlock = function(block) { if (this.typedBlocksDB_[block.type].length === 0) { delete this.typedBlocksDB_[block.type]; } -} +}; -/** - * Finds the blocks with the associated type and returns them. Blocks are +/** + * Finds the blocks with the associated type and returns them. Blocks are * optionally sorted by position; top to bottom (with slight LTR or RTL bias). * @param {string} type The type of block to search for. * @param {boolean} ordered Sor the list if true. @@ -228,7 +228,7 @@ Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { return []; } if (ordered && blocks.length > 1) { - var offset = + var offset = Math.sign(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); if (this.RTL) { offset *= -1; @@ -240,7 +240,7 @@ Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { }); } return blocks; -} +}; /** * Add a comment to the list of top comments. From 162900fdc67fa3abe8a0bc370253cee0dc49fd32 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 20 Nov 2018 13:24:44 -0800 Subject: [PATCH 03/11] eslint cleanup 2 --- core/block.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/block.js b/core/block.js index af37af82490..5f0f12d4865 100644 --- a/core/block.js +++ b/core/block.js @@ -673,16 +673,16 @@ Blockly.Block.prototype.setMovable = function(movable) { }; /** - * Get whether is block is duplicatable or not. If any decendent is not + * Get whether is block is duplicatable or not. If any decendent is not * duplicatable this block is not duplicatable. If duplicating this block and * decendents will put this block over the workspace\'s capacity this block is - * not duplicatable. If duplicating this block and decendents will put any + * not duplicatable. If duplicating this block and decendents will put any * decendent over their maxInstances this block is not duplicatable. * @return {boolean} True if duplicatable */ Blockly.Block.prototype.isDuplicatable = function() { var instancesOfType = this.workspace.getBlocksByType(this.type); - if(instancesOfType.length >= this.maxInstances) { + if (instancesOfType.length >= this.maxInstances) { return false; } @@ -693,7 +693,7 @@ Blockly.Block.prototype.isDuplicatable = function() { copyableBlocks.splice(index, copyableBlocks.length - index); } - if(copyableBlocks.length > this.workspace.remainingCapacity()) { + if (copyableBlocks.length > this.workspace.remainingCapacity()) { return false; } @@ -721,7 +721,7 @@ Blockly.Block.prototype.isDuplicatable = function() { } } return true; -} +}; /** * Get whether this block is a shadow block or not. From 5ebfd6716fe205c831806cb789bf62d57c660c73 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 21 Nov 2018 16:26:54 -0800 Subject: [PATCH 04/11] Moved maxInstances property from block to workspace (as a map of block type to max instances). isDuplicate() changed to correctly handle siblings/branches. --- core/block.js | 57 +++++++++++---------------------------------- core/flyout_base.js | 3 +-- core/options.js | 1 + core/workspace.js | 15 +++++++++++- 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/core/block.js b/core/block.js index 5f0f12d4865..f3f823e6f08 100644 --- a/core/block.js +++ b/core/block.js @@ -67,8 +67,6 @@ Blockly.Block = function(workspace, prototypeName, opt_id) { workspace.blockDB_[this.id] = this; /** @type {Blockly.Connection} */ this.outputConnection = null; - /** @type {number} */ - this.maxInstances = Infinity; /** @type {Blockly.Connection} */ this.nextConnection = null; /** @type {Blockly.Connection} */ @@ -673,52 +671,38 @@ Blockly.Block.prototype.setMovable = function(movable) { }; /** - * Get whether is block is duplicatable or not. If any decendent is not - * duplicatable this block is not duplicatable. If duplicating this block and + * Get whether is block is duplicatable or not. If duplicating this block and * decendents will put this block over the workspace\'s capacity this block is * not duplicatable. If duplicating this block and decendents will put any - * decendent over their maxInstances this block is not duplicatable. + * type over their maxInstances this block is not duplicatable. * @return {boolean} True if duplicatable */ Blockly.Block.prototype.isDuplicatable = function() { - var instancesOfType = this.workspace.getBlocksByType(this.type); - if (instancesOfType.length >= this.maxInstances) { - return false; - } - var copyableBlocks = this.getDescendants(true); // Remove all "next statement" blocks because they will not be copied. if (this.getNextBlock()) { var index = copyableBlocks.indexOf(this.getNextBlock()); copyableBlocks.splice(index, copyableBlocks.length - index); } - + if (copyableBlocks.length > this.workspace.remainingCapacity()) { return false; } - // If we do not have any blocks to copy besides ourself, we can just - // return true, because we already checked ourself. - if (copyableBlocks.length > 1) { - var type = this.type; - var copyableBlocksOfType = copyableBlocks.filter(function(block) { - return block.type == type; - }); - if (copyableBlocksOfType.length + instancesOfType.length > - this.maxInstances) { - return false; + var checkedTypes = []; + for (var i = 0, checkBlock; checkBlock = copyableBlocks[i]; i++) { + if (checkedTypes.includes(checkBlock.type)) { + continue; } - var children = this.getChildren(false); - for (var i = 0; i < children.length; i++) { - // No need to check the next block because it will not be duplicated - if (children[i] === this.getNextBlock()) { - continue; - } - if (!children[i].isDuplicatable()) { - return false; - } + var copyableBlocksOfType = copyableBlocks.filter(function(copyableBlock) { + return copyableBlock.type == checkBlock.type; + }); + if (copyableBlocksOfType.length > + this.workspace.remainingCapacityOfType(checkBlock.type)) { + return false; } + checkedTypes.push(checkBlock.type); } return true; }; @@ -1126,16 +1110,6 @@ Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { } }; -/** - * Set the maximum number of instances of this block allowed at a time. - * @param {number} maxInstances The maximum number of instances. - */ -Blockly.Block.prototype.setMaxInstances = function(maxInstances) { - if (maxInstances) { - this.maxInstances = maxInstances; - } -}; - /** * Set whether value inputs are arranged horizontally or vertically. * @param {boolean} newBoolean True if inputs are horizontal. @@ -1329,9 +1303,6 @@ Blockly.Block.prototype.jsonInit = function(json) { if (json['output'] !== undefined) { this.setOutput(true, json['output']); } - if (json['maxInstances'] !== undefined) { - this.setMaxInstances(json['maxInstances']); - } if (json['previousStatement'] !== undefined) { this.setPreviousStatement(true, json['previousStatement']); } diff --git a/core/flyout_base.js b/core/flyout_base.js index 33bc005d6c6..aa315118c75 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -731,8 +731,7 @@ Blockly.Flyout.prototype.filterForCapacity_ = function() { if (this.permanentlyDisabled_.indexOf(block) == -1) { var allBlocks = block.getDescendants(false); block.setDisabled(allBlocks.length > remainingCapacity || - this.targetWorkspace_.getBlocksByType(block.type).length >= - block.maxInstances); + this.targetWorkspace_.remainingCapacityOfType(block.type) <= 0); } } }; diff --git a/core/options.js b/core/options.js index 51a47a1ce04..0b91d9d647e 100644 --- a/core/options.js +++ b/core/options.js @@ -119,6 +119,7 @@ Blockly.Options = function(options) { this.disable = hasDisable; this.readOnly = readOnly; this.maxBlocks = options['maxBlocks'] || Infinity; + this.maxInstances = options['maxInstances']; this.pathToMedia = pathToMedia; this.hasCategories = hasCategories; this.hasScrollbars = hasScrollbars; diff --git a/core/workspace.js b/core/workspace.js index 124a2ae940b..4e42efc58ca 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -218,7 +218,7 @@ Blockly.Workspace.prototype.removeTypedBlock = function(block) { * Finds the blocks with the associated type and returns them. Blocks are * optionally sorted by position; top to bottom (with slight LTR or RTL bias). * @param {string} type The type of block to search for. - * @param {boolean} ordered Sor the list if true. + * @param {boolean} ordered Sort the list if true. * @return {!Array.} The blocks of the given type. */ Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { @@ -508,6 +508,19 @@ Blockly.Workspace.prototype.remainingCapacity = function() { return this.options.maxBlocks - this.getAllBlocks().length; }; +/** + * The number of blocks of the given type that may be added to the workspace + * before reaching the maxInstances allowed for that type. + * @return {number} Number of blocks of type left. + */ +Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { + if (!this.options.maxInstances) { + return Infinity; + } + return (this.options.maxInstances[type] || Infinity) - + this.getBlocksByType(type).length; +}; + /** * Undo or redo the previous action. * @param {boolean} redo False if undo, true if redo. From a581b5a4ffd2bc69e810eeacee97e05bf69dc1bc Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 21 Nov 2018 17:00:27 -0800 Subject: [PATCH 05/11] eslint cleanup --- core/workspace.js | 1 + 1 file changed, 1 insertion(+) diff --git a/core/workspace.js b/core/workspace.js index 4e42efc58ca..4cc09ed1071 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -511,6 +511,7 @@ Blockly.Workspace.prototype.remainingCapacity = function() { /** * The number of blocks of the given type that may be added to the workspace * before reaching the maxInstances allowed for that type. + * @param {string} type Type of block to return capacity for. * @return {number} Number of blocks of type left. */ Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { From d0a9fc449568a55f3f805874a6a5b644b8f3eaf3 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Mon, 26 Nov 2018 14:27:53 -0800 Subject: [PATCH 06/11] Changed checking types to map. Added hasBlockLimits. Fixed Nits. --- core/block.js | 28 +++++++++++++++------------- core/blockly.js | 2 +- core/workspace.js | 23 ++++++++++++++++------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/core/block.js b/core/block.js index f3f823e6f08..37a0483af93 100644 --- a/core/block.js +++ b/core/block.js @@ -672,16 +672,21 @@ Blockly.Block.prototype.setMovable = function(movable) { /** * Get whether is block is duplicatable or not. If duplicating this block and - * decendents will put this block over the workspace\'s capacity this block is + * descendants will put this block over the workspace's capacity this block is * not duplicatable. If duplicating this block and decendents will put any * type over their maxInstances this block is not duplicatable. - * @return {boolean} True if duplicatable + * @return {boolean} True if duplicatable. */ Blockly.Block.prototype.isDuplicatable = function() { + if (!this.workspace.hasBlockLimits()) { + return true; + } + var copyableBlocks = this.getDescendants(true); // Remove all "next statement" blocks because they will not be copied. - if (this.getNextBlock()) { - var index = copyableBlocks.indexOf(this.getNextBlock()); + var nextBlock = this.getNextBlock(); + if (nextBlock) { + var index = copyableBlocks.indexOf(nextBlock); copyableBlocks.splice(index, copyableBlocks.length - index); } @@ -689,20 +694,17 @@ Blockly.Block.prototype.isDuplicatable = function() { return false; } - var checkedTypes = []; + var copyableBlocksTypeCounts = {}; for (var i = 0, checkBlock; checkBlock = copyableBlocks[i]; i++) { - if (checkedTypes.includes(checkBlock.type)) { - continue; + if (copyableBlocksTypeCounts[checkBlock.type]) { + copyableBlocksTypeCounts[checkBlock.type]++; + } else { + copyableBlocksTypeCounts[checkBlock.type] = 1; } - - var copyableBlocksOfType = copyableBlocks.filter(function(copyableBlock) { - return copyableBlock.type == checkBlock.type; - }); - if (copyableBlocksOfType.length > + if (copyableBlocksTypeCounts[checkBlock.type] > this.workspace.remainingCapacityOfType(checkBlock.type)) { return false; } - checkedTypes.push(checkBlock.type); } return true; }; diff --git a/core/blockly.js b/core/blockly.js index bda6daa07c8..435e7555f5a 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -99,7 +99,7 @@ Blockly.clipboardXml_ = null; Blockly.clipboardSource_ = null; /** - * Copied object. + * Object copied onto the clipboard. * @type {Blockly.Block | Blockly.WorkspaceComment} * @private */ diff --git a/core/workspace.js b/core/workspace.js index 4cc09ed1071..f8afa09cb91 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -179,7 +179,7 @@ Blockly.Workspace.prototype.removeTopBlock = function(block) { Blockly.Workspace.prototype.getTopBlocks = function(ordered) { // Copy the topBlocks_ list. var blocks = [].concat(this.topBlocks_); - if (ordered && blocks.length > 1) { + if (ordered && blocks.length > 1) { var offset = Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); if (this.RTL) { @@ -208,8 +208,9 @@ Blockly.Workspace.prototype.addTypedBlock = function(block) { * @param {!Blockly.Block} block Block to remove. */ Blockly.Workspace.prototype.removeTypedBlock = function(block) { - this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type].indexOf(block), 1); - if (this.typedBlocksDB_[block.type].length === 0) { + this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type] + .indexOf(block), 1); + if (!this.typedBlocksDB_[block.type].length) { delete this.typedBlocksDB_[block.type]; } }; @@ -222,11 +223,10 @@ Blockly.Workspace.prototype.removeTypedBlock = function(block) { * @return {!Array.} The blocks of the given type. */ Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { - if (this.typedBlocksDB_[type]) { - var blocks = [].concat(this.typedBlocksDB_[type]); - } else { + if (!this.typedBlocksDB_[type]) { return []; } + var blocks = this.typedBlocksDB_[type].slice(0); if (ordered && blocks.length > 1) { var offset = Math.sign(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); @@ -510,7 +510,7 @@ Blockly.Workspace.prototype.remainingCapacity = function() { /** * The number of blocks of the given type that may be added to the workspace - * before reaching the maxInstances allowed for that type. + * before reaching the maxInstances allowed for that type. * @param {string} type Type of block to return capacity for. * @return {number} Number of blocks of type left. */ @@ -522,6 +522,15 @@ Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { this.getBlocksByType(type).length; }; +/** + * Checks if the workspace has any limits on the maximum number of blocks, + * or the maximum number of blocks of specific types. + * @returns {boolean} True if it has block limits, false otherwise. + */ +Blockly.Workspace.prototype.hasBlockLimits = function() { + return this.options.maxBlocks != Infinity || !!this.options.maxInstances; +} + /** * Undo or redo the previous action. * @param {boolean} redo False if undo, true if redo. From 7a6483263c05b5f813ce08f8d0108fae7884524c Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Mon, 26 Nov 2018 14:45:14 -0800 Subject: [PATCH 07/11] Added limit_instances test block. eslint fixes. --- core/workspace.js | 6 +++--- tests/blocks/test_blocks.js | 38 ++++++++++++++++++++++++++----------- tests/playground.html | 2 ++ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/core/workspace.js b/core/workspace.js index f8afa09cb91..175406b7a90 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -179,7 +179,7 @@ Blockly.Workspace.prototype.removeTopBlock = function(block) { Blockly.Workspace.prototype.getTopBlocks = function(ordered) { // Copy the topBlocks_ list. var blocks = [].concat(this.topBlocks_); - if (ordered && blocks.length > 1) { + if (ordered && blocks.length > 1) { var offset = Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); if (this.RTL) { @@ -209,7 +209,7 @@ Blockly.Workspace.prototype.addTypedBlock = function(block) { */ Blockly.Workspace.prototype.removeTypedBlock = function(block) { this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type] - .indexOf(block), 1); + .indexOf(block), 1); if (!this.typedBlocksDB_[block.type].length) { delete this.typedBlocksDB_[block.type]; } @@ -529,7 +529,7 @@ Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { */ Blockly.Workspace.prototype.hasBlockLimits = function() { return this.options.maxBlocks != Infinity || !!this.options.maxInstances; -} +}; /** * Undo or redo the previous action. diff --git a/tests/blocks/test_blocks.js b/tests/blocks/test_blocks.js index 2698e7e17f7..f094b1131de 100644 --- a/tests/blocks/test_blocks.js +++ b/tests/blocks/test_blocks.js @@ -44,6 +44,22 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT "output": null, "colour": 230 }, + { + "type": "limit_instances", + "message0": "limit 3 instances %1 %2", + "args0": [ + { + "type": "input_dummy" + }, + { + "type": "input_statement", + "name": "STATEMENT" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 230, + }, { "type": "example_dropdown_long", "message0": "long: %1", @@ -145,17 +161,17 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT "type": "example_angle", "message0": "angle: %1", "args0": [ - { - "type": "field_angle", - "name": "FIELDNAME", - "angle": "90", - "alt": - { - "type": "field_label", - "text": "NO ANGLE FIELD" - } - } - ] + { + "type": "field_angle", + "name": "FIELDNAME", + "angle": "90", + "alt": + { + "type": "field_label", + "text": "NO ANGLE FIELD" + } + } + ] }, { "type": "example_date", diff --git a/tests/playground.html b/tests/playground.html index 20a216c7388..d2f5d50a749 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -103,6 +103,7 @@ }, horizontalLayout: side == 'top' || side == 'bottom', maxBlocks: Infinity, + maxInstances: {'limit_instances': 3}, media: '../media/', oneBasedIndex: true, readOnly: false, @@ -1080,6 +1081,7 @@

Blockly Playground

+ From 110b84a984b95c1b18767e90c9f8aec264960524 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Mon, 26 Nov 2018 21:35:15 -0800 Subject: [PATCH 08/11] fixup! Added limit_instances test block. eslint fixes. --- .eslintrc | 68 ------------------------------------------------------ .gitignore | 1 + 2 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index b20da09f8d3..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,68 +0,0 @@ -{ - "rules": { - "curly": ["error"], - "eol-last": ["error"], - "indent": [ - "error", 2, # Blockly/Google use 2-space indents - # Blockly/Google uses +4 space indents for line continuations. - { - "SwitchCase": 1, - "MemberExpression": 2, - "ObjectExpression": 1, - "FunctionDeclaration": { - "body": 1, - "parameters": 2 - }, - "FunctionExpression": { - "body": 1, - "parameters": 2 - }, - "CallExpression": { - "arguments": 2 - }, - # Ignore default rules for ternary expressions. - "ignoredNodes": ["ConditionalExpression"] - } - ], - "keyword-spacing": ["error"], - "linebreak-style": ["error", "unix"], - "max-len": [ - "error", - { - "code": 100, - "tabWidth": 4, - "ignoreStrings": true, - "ignoreRegExpLiterals": true - } - ], - "no-trailing-spaces": ["error", { "skipBlankLines": true }], - "no-unused-vars": [ - "error", - { - "args": "after-used", - # Ignore vars starting with an underscore. - "varsIgnorePattern": "^_", - # Ignore arguments starting with an underscore. - "argsIgnorePattern": "^_" - } - ], - "no-use-before-define": ["error"], - "quotes": ["off"], # Blockly uses single quotes except for JSON blobs, which must use double quotes. - "semi": ["error", "always"], - "space-before-function-paren": ["error", "never"], # Blockly doesn't have space before function paren - "space-infix-ops": ["error"], - "strict": ["off"], # Blockly uses 'use strict' in files - "no-cond-assign": ["off"], # Blockly often uses cond-assignment in loops - "no-redeclare": ["off"], # Closure style allows redeclarations - "valid-jsdoc": ["error", {"requireReturn": false}], - "no-console": ["off"] - }, - "env": { - "browser": true - }, - "globals": { - "Blockly": true, # Blockly global - "goog": true # goog closure libraries/includes - }, - "extends": "eslint:recommended" -} diff --git a/.gitignore b/.gitignore index 3277b7174ee..05c86c5d3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ tests/compile/main_compressed.js tests/compile/*compiler*.jar local_build/*compiler*.jar local_build/local_blockly_compressed.js +.idea/ From b810bc2bf87428c445ff698b69714155d770cf61 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 27 Nov 2018 12:51:40 -0800 Subject: [PATCH 09/11] Changed sorting objects to a private static function of the workspace. Fixed nits. Undeleted .eslintrc --- .eslintrc | 68 +++++++++++++++++++++++++++++++++++++++++++++++ core/block.js | 4 +-- core/workspace.js | 52 ++++++++++++++++++++---------------- 3 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000000..b20da09f8d3 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,68 @@ +{ + "rules": { + "curly": ["error"], + "eol-last": ["error"], + "indent": [ + "error", 2, # Blockly/Google use 2-space indents + # Blockly/Google uses +4 space indents for line continuations. + { + "SwitchCase": 1, + "MemberExpression": 2, + "ObjectExpression": 1, + "FunctionDeclaration": { + "body": 1, + "parameters": 2 + }, + "FunctionExpression": { + "body": 1, + "parameters": 2 + }, + "CallExpression": { + "arguments": 2 + }, + # Ignore default rules for ternary expressions. + "ignoredNodes": ["ConditionalExpression"] + } + ], + "keyword-spacing": ["error"], + "linebreak-style": ["error", "unix"], + "max-len": [ + "error", + { + "code": 100, + "tabWidth": 4, + "ignoreStrings": true, + "ignoreRegExpLiterals": true + } + ], + "no-trailing-spaces": ["error", { "skipBlankLines": true }], + "no-unused-vars": [ + "error", + { + "args": "after-used", + # Ignore vars starting with an underscore. + "varsIgnorePattern": "^_", + # Ignore arguments starting with an underscore. + "argsIgnorePattern": "^_" + } + ], + "no-use-before-define": ["error"], + "quotes": ["off"], # Blockly uses single quotes except for JSON blobs, which must use double quotes. + "semi": ["error", "always"], + "space-before-function-paren": ["error", "never"], # Blockly doesn't have space before function paren + "space-infix-ops": ["error"], + "strict": ["off"], # Blockly uses 'use strict' in files + "no-cond-assign": ["off"], # Blockly often uses cond-assignment in loops + "no-redeclare": ["off"], # Closure style allows redeclarations + "valid-jsdoc": ["error", {"requireReturn": false}], + "no-console": ["off"] + }, + "env": { + "browser": true + }, + "globals": { + "Blockly": true, # Blockly global + "goog": true # goog closure libraries/includes + }, + "extends": "eslint:recommended" +} diff --git a/core/block.js b/core/block.js index 37a0483af93..e27dc808efe 100644 --- a/core/block.js +++ b/core/block.js @@ -673,7 +673,7 @@ Blockly.Block.prototype.setMovable = function(movable) { /** * Get whether is block is duplicatable or not. If duplicating this block and * descendants will put this block over the workspace's capacity this block is - * not duplicatable. If duplicating this block and decendents will put any + * not duplicatable. If duplicating this block and descendants will put any * type over their maxInstances this block is not duplicatable. * @return {boolean} True if duplicatable. */ @@ -694,7 +694,7 @@ Blockly.Block.prototype.isDuplicatable = function() { return false; } - var copyableBlocksTypeCounts = {}; + var copyableBlocksTypeCounts = Object.create(null); for (var i = 0, checkBlock; checkBlock = copyableBlocks[i]; i++) { if (copyableBlocksTypeCounts[checkBlock.type]) { copyableBlocksTypeCounts[checkBlock.type]++; diff --git a/core/workspace.js b/core/workspace.js index 175406b7a90..88947e0e7be 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -152,6 +152,22 @@ Blockly.Workspace.prototype.dispose = function() { */ Blockly.Workspace.SCAN_ANGLE = 3; +/** + * Compare function for sorting objects (blocks, comments, etc) by position; + * top to bottom (with slight LTR or RTL bias). + * @param {!Blockly.Block | !Blockly.WorkspaceComment} a The first object to compare. + * @param {!Blockly.Block | !Blockly.WorkspaceComment} b The second object to compare. + * @returns {number} The comparison value. This tells Array.sort() how to change + * object a's index. + * @private + */ +Blockly.Workspace.prototype.sortObjects_ = function(a, b) { + var aXY = a.getRelativeToSurfaceXY(); + var bXY = b.getRelativeToSurfaceXY(); + return (aXY.y + Blockly.Workspace.prototype.sortObjects_.offset * aXY.x) - + (bXY.y + Blockly.Workspace.prototype.sortObjects_.offset * bXY.x); +}; + /** * Add a block to the list of top blocks. * @param {!Blockly.Block} block Block to add. @@ -180,21 +196,18 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) { // Copy the topBlocks_ list. var blocks = [].concat(this.topBlocks_); if (ordered && blocks.length > 1) { - var offset = + this.sortObjects_.offset = Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); if (this.RTL) { - offset *= -1; + this.sortObjects_.offset *= -1; } - blocks.sort(function(a, b) { - var aXY = a.getRelativeToSurfaceXY(); - var bXY = b.getRelativeToSurfaceXY(); - return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); - }); + blocks.sort(this.sortObjects_); } return blocks; }; -/** Add a block to the list of blocks keyed by type. +/** + * Add a block to the list of blocks keyed by type. * @param {!Blockly.Block} block Block to add. */ Blockly.Workspace.prototype.addTypedBlock = function(block) { @@ -204,7 +217,8 @@ Blockly.Workspace.prototype.addTypedBlock = function(block) { this.typedBlocksDB_[block.type].push(block); }; -/** Remove a block from the list of blocks keyed by type. +/** + * Remove a block from the list of blocks keyed by type. * @param {!Blockly.Block} block Block to remove. */ Blockly.Workspace.prototype.removeTypedBlock = function(block) { @@ -228,16 +242,12 @@ Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) { } var blocks = this.typedBlocksDB_[type].slice(0); if (ordered && blocks.length > 1) { - var offset = + this.sortObjects_.offset = Math.sign(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); if (this.RTL) { - offset *= -1; + this.sortObjects_.offset *= -1; } - blocks.sort(function(a, b) { - var aXY = a.getRelativeToSurfaceXY(); - var bXY = b.getRelativeToSurfaceXY(); - return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); - }); + blocks.sort(this.sortObjects_); } return blocks; }; @@ -285,16 +295,12 @@ Blockly.Workspace.prototype.getTopComments = function(ordered) { // Copy the topComments_ list. var comments = [].concat(this.topComments_); if (ordered && comments.length > 1) { - var offset = + this.sortObjects_.offset = Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE)); if (this.RTL) { - offset *= -1; + this.sortObjects_.offset *= -1; } - comments.sort(function(a, b) { - var aXY = a.getRelativeToSurfaceXY(); - var bXY = b.getRelativeToSurfaceXY(); - return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x); - }); + comments.sort(this.sortObjects_); } return comments; }; From 86d0a20c21a30062740ead5e92d62a3910501d73 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 27 Nov 2018 13:27:13 -0800 Subject: [PATCH 10/11] Reverted .gitignore file. --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 05c86c5d3b0..c546855b886 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ npm-debug.log tests/compile/main_compressed.js tests/compile/*compiler*.jar local_build/*compiler*.jar -local_build/local_blockly_compressed.js -.idea/ +local_build/local_blockly_compressed.js \ No newline at end of file From 17584eefecc8b791a4bdcac35415223294750ae0 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 27 Nov 2018 16:01:10 -0800 Subject: [PATCH 11/11] Added getBlockTypeCounts() to utils. Added isCapacityAvailable() to workspace. Changed clipboard to save typeCountsMap rather than object. --- core/block.js | 28 ++-------------------------- core/blockly.js | 21 ++++++++++++--------- core/utils.js | 29 +++++++++++++++++++++++++++++ core/workspace.js | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/core/block.js b/core/block.js index e27dc808efe..c5cea853b80 100644 --- a/core/block.js +++ b/core/block.js @@ -681,32 +681,8 @@ Blockly.Block.prototype.isDuplicatable = function() { if (!this.workspace.hasBlockLimits()) { return true; } - - var copyableBlocks = this.getDescendants(true); - // Remove all "next statement" blocks because they will not be copied. - var nextBlock = this.getNextBlock(); - if (nextBlock) { - var index = copyableBlocks.indexOf(nextBlock); - copyableBlocks.splice(index, copyableBlocks.length - index); - } - - if (copyableBlocks.length > this.workspace.remainingCapacity()) { - return false; - } - - var copyableBlocksTypeCounts = Object.create(null); - for (var i = 0, checkBlock; checkBlock = copyableBlocks[i]; i++) { - if (copyableBlocksTypeCounts[checkBlock.type]) { - copyableBlocksTypeCounts[checkBlock.type]++; - } else { - copyableBlocksTypeCounts[checkBlock.type] = 1; - } - if (copyableBlocksTypeCounts[checkBlock.type] > - this.workspace.remainingCapacityOfType(checkBlock.type)) { - return false; - } - } - return true; + return this.workspace.isCapacityAvailable( + Blockly.utils.getBlockTypeCounts(this, true)); }; /** diff --git a/core/blockly.js b/core/blockly.js index 435e7555f5a..366e55fdf78 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -99,11 +99,11 @@ Blockly.clipboardXml_ = null; Blockly.clipboardSource_ = null; /** - * Object copied onto the clipboard. - * @type {Blockly.Block | Blockly.WorkspaceComment} + * Map of types to type counts for the clipboard object and descendants. + * @type {Object} * @private */ -Blockly.copiedObject_ = null; +Blockly.clipboardTypeCounts_ = null; /** * Cached value for whether 3D is supported. @@ -233,17 +233,19 @@ Blockly.onKeyDown_ = function(e) { } if (e.keyCode == 86) { // 'v' for paste. - if (Blockly.clipboardXml_ && (Blockly.copiedObject_.isComment || - Blockly.copiedObject_.isDuplicatable())) { - Blockly.Events.setGroup(true); + if (Blockly.clipboardXml_) { // Pasting always pastes to the main workspace, even if the copy // started in a flyout workspace. var workspace = Blockly.clipboardSource_; if (workspace.isFlyout) { workspace = workspace.targetWorkspace; } - workspace.paste(Blockly.clipboardXml_); - Blockly.Events.setGroup(false); + if (Blockly.clipboardTypeCounts_ && + workspace.isCapacityAvailable(Blockly.clipboardTypeCounts_)) { + Blockly.Events.setGroup(true); + workspace.paste(Blockly.clipboardXml_); + Blockly.Events.setGroup(false); + } } } else if (e.keyCode == 90) { // 'z' for undo 'Z' is for redo. @@ -281,7 +283,8 @@ Blockly.copy_ = function(toCopy) { } Blockly.clipboardXml_ = xml; Blockly.clipboardSource_ = toCopy.workspace; - Blockly.copiedObject_ = toCopy; + Blockly.clipboardTypeCounts_ = toCopy.isComment ? null : + Blockly.utils.getBlockTypeCounts(toCopy, true); }; /** diff --git a/core/utils.js b/core/utils.js index 8d5ccac8376..2ec39feee81 100644 --- a/core/utils.js +++ b/core/utils.js @@ -974,3 +974,32 @@ Blockly.utils.containsNode = function(parent, descendant) { return !!(parent.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); }; + +/** + * Get a map of all the block's descendants mapping their type to the number of + * children with that type. + * @param {!Blockly.Block} block The block to map. + * @param {boolean=} opt_stripFollowing Optionally ignore all following + * statements (blocks that are not inside a value or statement input + * of the block). + * @returns {!Object} Map of types to type counts for descendants of the bock. + */ +Blockly.utils.getBlockTypeCounts = function(block, opt_stripFollowing) { + var typeCountsMap = Object.create(null); + var descendants = block.getDescendants(true); + if (opt_stripFollowing) { + var nextBlock = block.getNextBlock(); + if (nextBlock) { + var index = descendants.indexOf(nextBlock); + descendants.splice(index, descendants.length - index); + } + } + for (var i = 0, checkBlock; checkBlock = descendants[i]; i++) { + if (typeCountsMap[checkBlock.type]) { + typeCountsMap[checkBlock.type]++; + } else { + typeCountsMap[checkBlock.type] = 1; + } + } + return typeCountsMap; +}; diff --git a/core/workspace.js b/core/workspace.js index 88947e0e7be..8d58ff87abb 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -155,8 +155,10 @@ Blockly.Workspace.SCAN_ANGLE = 3; /** * Compare function for sorting objects (blocks, comments, etc) by position; * top to bottom (with slight LTR or RTL bias). - * @param {!Blockly.Block | !Blockly.WorkspaceComment} a The first object to compare. - * @param {!Blockly.Block | !Blockly.WorkspaceComment} b The second object to compare. + * @param {!Blockly.Block | !Blockly.WorkspaceComment} a The first object to + * compare. + * @param {!Blockly.Block | !Blockly.WorkspaceComment} b The second object to + * compare. * @returns {number} The comparison value. This tells Array.sort() how to change * object a's index. * @private @@ -528,6 +530,33 @@ Blockly.Workspace.prototype.remainingCapacityOfType = function(type) { this.getBlocksByType(type).length; }; +/** + * Check if there is remaining capacity for blocks of the given counts to be + * created. If the total number of blocks represented by the map is more than + * the total remaining capacity, it returns false. If a type count is more + * than the remaining capacity for that type, it returns false. + * @param {!Object} typeCountsMap A map of types to counts (usually representing + * blocks to be created). + * @returns {boolean} True if there is capacity for the given map, + * false otherwise. + */ +Blockly.Workspace.prototype.isCapacityAvailable = function(typeCountsMap) { + if (!this.hasBlockLimits()) { + return true; + } + var copyableBlocksCount = 0; + for (var type in typeCountsMap) { + if (typeCountsMap[type] > this.remainingCapacityOfType(type)) { + return false; + } + copyableBlocksCount += typeCountsMap[type]; + } + if (copyableBlocksCount > this.remainingCapacity()) { + return false; + } + return true; +}; + /** * Checks if the workspace has any limits on the maximum number of blocks, * or the maximum number of blocks of specific types.