diff --git a/.github/workflows/develop-builds.yml b/.github/workflows/develop-builds.yml new file mode 100644 index 0000000000..73d73a3ca2 --- /dev/null +++ b/.github/workflows/develop-builds.yml @@ -0,0 +1,51 @@ +name: Build and Publish to develop-builds + +on: + push: + branches: + - develop + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout develop branch + uses: actions/checkout@v4 + with: + ref: develop + fetch-depth: 0 # Fetch all history for all branches + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies and build + run: | + npm ci # This triggers prepublish which builds compressed files + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Checkout develop-builds branch + run: | + git fetch origin develop-builds:develop-builds || git checkout -b develop-builds + git checkout develop-builds + + - name: Merge develop into develop-builds + run: | + git merge origin/develop --no-edit || true + + - name: Add compiled files + run: | + git add -f blockly_compressed*.js blocks_compressed*.js msg/*.js + git diff --staged --quiet || git commit -m "Auto-build: Update compiled files from develop ($(git rev-parse --short origin/develop))" + + - name: Push to develop-builds + run: | + git push origin develop-builds diff --git a/blocks_common/custom.js b/blocks_common/custom.js new file mode 100644 index 0000000000..6a2b04f71c --- /dev/null +++ b/blocks_common/custom.js @@ -0,0 +1,34 @@ +/** + * @fileoverview Custom blocks for Blockly. + * @author @SharkPool-SP (SharkPool) + */ +'use strict'; + +goog.provide('Blockly.Blocks.customInput'); + +goog.require('Blockly.Blocks'); + +goog.require('Blockly.Colours'); + +goog.require('Blockly.constants'); + +Blockly.Blocks['customInput'] = { + /** + * Block for custom inputs. + * @this Blockly.Block + */ + init: function() { + this.jsonInit({ + "message0": "%1", + "args0": [ + { + "type": "field_customInput", + "name": "CUSTOM" + } + ], + "outputShape": Blockly.OUTPUT_SHAPE_SQUARE, + "output": "String", + "extensions": ["colours_pen"] + }); + } +}; diff --git a/blocks_vertical/data.js b/blocks_vertical/data.js index 726697d47d..bb09efbece 100644 --- a/blocks_vertical/data.js +++ b/blocks_vertical/data.js @@ -48,7 +48,9 @@ Blockly.Blocks['data_variable'] = { ], "category": Blockly.Categories.data, "checkboxInFlyout": true, - "extensions": ["contextMenu_getVariableBlock", "colours_data", "output_string"] + // ob: allow variables in any input, like AmpMod and pang and joe's epic tw mod + "output": null, + "extensions": ["contextMenu_getVariableBlock", "colours_data", "shape_reporter"] }); } }; diff --git a/blocks_vertical/operators.js b/blocks_vertical/operators.js index 1caa04481c..980b4aec24 100644 --- a/blocks_vertical/operators.js +++ b/blocks_vertical/operators.js @@ -171,7 +171,45 @@ Blockly.Blocks['operator_lt'] = { }); } }; +Blockly.Blocks['operator_gtoreq'] = { + init: function() { + this.jsonInit({ + "message0": "%1 ≥ %2", + "args0": [ + { + "type": "input_value", + "name": "OPERAND1" + }, + { + "type": "input_value", + "name": "OPERAND2" + } + ], + "category": Blockly.Categories.operators, + "extensions": ["colours_operators", "output_boolean"] + }); + } +}; +Blockly.Blocks['operator_ltoreq'] = { + init: function() { + this.jsonInit({ + "message0": "%1 ≤ %2", + "args0": [ + { + "type": "input_value", + "name": "OPERAND1" + }, + { + "type": "input_value", + "name": "OPERAND2" + } + ], + "category": Blockly.Categories.operators, + "extensions": ["colours_operators", "output_boolean"] + }); + } +}; Blockly.Blocks['operator_equals'] = { /** * Block for equals comparator. diff --git a/blocks_vertical/vertical_extensions.js b/blocks_vertical/vertical_extensions.js index 96e2684534..ceac4bc549 100644 --- a/blocks_vertical/vertical_extensions.js +++ b/blocks_vertical/vertical_extensions.js @@ -130,6 +130,17 @@ Blockly.ScratchBlocks.VerticalExtensions.OUTPUT_STRING = function() { this.setOutput(true, 'String'); }; +/** + * Extension to make represent a round, but generic, reporter in Scratch-Blocks. + * That means the block has inline inputs, and a round output shape. + * @this {Blockly.Block} + * @readonly + */ +Blockly.ScratchBlocks.VerticalExtensions.SHAPE_REPORTER = function() { + this.setInputsInline(true); + this.setOutputShape(Blockly.OUTPUT_SHAPE_ROUND); +}; + /** * Extension to make represent a boolean reporter in Scratch-Blocks. * That means the block has inline inputs, a round output shape, and a 'Boolean' @@ -258,6 +269,8 @@ Blockly.ScratchBlocks.VerticalExtensions.registerAll = function() { Blockly.ScratchBlocks.VerticalExtensions.SHAPE_HAT); Blockly.Extensions.register('shape_end', Blockly.ScratchBlocks.VerticalExtensions.SHAPE_END); + Blockly.Extensions.register('shape_reporter', + Blockly.ScratchBlocks.VerticalExtensions.SHAPE_REPORTER); // Output shapes and types are related. Blockly.Extensions.register('output_number', diff --git a/core/block.js b/core/block.js index ae49a86015..64089f7c27 100644 --- a/core/block.js +++ b/core/block.js @@ -47,6 +47,7 @@ goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.math.Coordinate'); goog.require('goog.string'); +goog.require('Blockly.FieldCustom'); /** diff --git a/core/blockly.js b/core/blockly.js index f98e53ad62..43858170c1 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -64,6 +64,7 @@ goog.require('Blockly.constants'); goog.require('Blockly.inject'); goog.require('Blockly.utils'); goog.require('goog.color'); +goog.require('Blockly.FieldCustom'); // Turn off debugging when compiled. diff --git a/core/field_customInput.js b/core/field_customInput.js new file mode 100644 index 0000000000..62ba790583 --- /dev/null +++ b/core/field_customInput.js @@ -0,0 +1,195 @@ +/** + * @fileoverview custom DOM-based input users can customize + * @author @SharkPool-SP (SharkPool) + */ +'use strict'; + +goog.provide('Blockly.FieldCustom'); + +const customInputs = new Map(); + +/** + * Class for a custom field. + * @param {object} options Object containing the default value, inputID, etc for the field + * @extends {Blockly.Field} + * @constructor + */ +Blockly.FieldCustom = function(options) { + Blockly.FieldCustom.superClass_.constructor.call(this, options); + this.addArgType('text'); + + /** + * input ID used to identify input from 'customInputs' + * @type {string} + */ + this.inputID = options.id ? options.id : null; + + /** + * value of the field + * @type {any} + */ + this.value_ = options.value ? options.value : ''; + /** + * input parts stored in 'customInputs' + * @type {object} + */ + this.inputParts = {}; + + /** + * Touch event wrapper. + * Runs when the field is selected. + * @type {!Array} + * @private + */ + this.mouseDownWrapper_ = null; +}; +goog.inherits(Blockly.FieldCustom, Blockly.Field); + +/** + * Construct a FieldCustom from a JSON arg object. + * @param {!Object} options A JSON object with options. + * @returns {!Blockly.FieldCustom} The new field instance. + * @package + * @nocollapse + */ +Blockly.FieldCustom.fromJson = function(options) { + return new Blockly.FieldCustom(options); +}; + +Blockly.FieldCustom.registerInput = function(id, templateHTML, onInit, onClick, onUpdate, optOnDispose) { + if (!id || typeof id !== 'string') { + console.warn('Param 1 must be a non-empty string id!'); + return; + } + if (customInputs.has && customInputs.has(id)) { + console.warn('An input with id "' + id + '" is already registered; overriding.'); + } + if (!templateHTML || !(templateHTML instanceof Node)) { + console.warn('Param 2 must be a valid DOM element!'); + return; + } + if (!onInit || typeof onInit !== 'function') { + console.warn('Param 3 must be a function!'); + return; + } + if (!onClick || typeof onClick !== 'function') { + console.warn('Param 4 must be a function!'); + return; + } + if (!onUpdate || typeof onUpdate !== 'function') { + console.warn('Param 5 must be a function!'); + return; + } + if (optOnDispose && typeof optOnDispose !== 'function') { + console.warn('Param 6 must be a function!'); + return; + } + customInputs.set(id, { templateHTML, onInit, onClick, onUpdate, optOnDispose }); +}; +Blockly.FieldCustom.unregisterInput = function(id) { + customInputs.delete(id); +}; +Blockly.FieldCustom.registeredInputs = function() { + return customInputs; +}; + +/** + * Called when the field is placed on a block. + * @param {Block} block The owning block. + */ +Blockly.FieldCustom.prototype.init = function() { + if (this.fieldGroup_) { + // custom field has already been initialized + return; + } + + this.inputParts = customInputs.get(this.inputID); + if (!this.inputParts) { + console.error(`No Custom Input found with ID '${this.inputID}', did you use 'registerInput'?`); + return; + } + + // Build the DOM. + const htmlDOM = this.inputParts.templateHTML.cloneNode(true); + htmlDOM.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); + this.inputParts.html = htmlDOM; // makes it easier for ext devs to find the input theyre editting + + this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null); + const boundingBox = htmlDOM.getBoundingClientRect(); + this.size_.width = htmlDOM.width ? htmlDOM.width : htmlDOM.style.width ? parseFloat(htmlDOM.style.width) : + boundingBox.width; + this.size_.height = htmlDOM.height ? htmlDOM.height : htmlDOM.style.height ? parseFloat(htmlDOM.style.height) : + Math.max(32, boundingBox.height); + + this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_); + + this.inputSource = Blockly.utils.createSvgElement('foreignObject', { + 'width': this.size_.width, 'height': this.size_.height, + 'pointer-events': 'all', 'cursor': 'pointer', 'overflow': 'visible' + }, this.fieldGroup_); + this.inputSource.appendChild(htmlDOM); + + this.mouseDownWrapper_ = Blockly.bindEventWithChecks_( + this.getClickTarget_(), 'mousedown', this, this.onMouseDown_ + ); + queueMicrotask(() => { + this.inputParts.onInit(this, this.inputParts.html); + }); +}; + +/** + * Set the value for this field + * @param {any} value The new value of whatever the user chooses + * @override + */ +Blockly.FieldCustom.prototype.setValue = function(value) { + if (!value || value === this.value_) { + return; // No change + } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.value_, value + )); + } + this.value_ = value; + if (this.inputParts !== undefined && this.inputParts.onUpdate) { + const htmlDOM = this.inputParts.html; + this.inputParts.onUpdate(this, htmlDOM); + } +}; + +/** + * Get the value from this field menu. + * @return {any} Current value. + */ +Blockly.FieldCustom.prototype.getValue = function() { + return this.value_; +}; + +/** + * do whatever the user desires on-edit + * @private + */ +Blockly.FieldCustom.prototype.showEditor_ = function() { + const htmlDOM = this.inputParts.html; + this.inputParts.onClick(this, htmlDOM); +}; + +/** + * Clean up this FieldCustom, as well as the inherited Field. + * @return {!Function} Closure to call on destruction of the WidgetDiv. + * @private + */ +Blockly.FieldCustom.prototype.dispose_ = function() { + var thisField = this; + return function() { + if (thisField.inputParts.optOnDispose) { + const htmlDOM = this.inputParts.html; + thisField.inputParts.optOnDispose(thisField, htmlDOM); + } + Blockly.FieldCustom.superClass_.dispose_.call(thisField)(); + if (thisField.mouseDownWrapper_) Blockly.unbindEvent_(thisField.mouseDownWrapper_); + }; +}; + +Blockly.Field.register('field_customInput', Blockly.FieldCustom);