diff --git a/core/block.js b/core/block.js index 75f41f1c3..e07e2337a 100644 --- a/core/block.js +++ b/core/block.js @@ -121,6 +121,10 @@ Blockly.Block = function(workspace, prototypeName, opt_id) { // Record initial inline state. /** @type {boolean|undefined} */ this.inputsInlineDefault = this.inputsInline; + if (Blockly.Events.isEnabled() && !this.isShadow()) { + var xmlBlock = Blockly.Xml.blockToDom(this); + Blockly.Events.fire(new Blockly.Events.Create(workspace, xmlBlock)); + } }; /** @@ -160,6 +164,10 @@ Blockly.Block.prototype.colour_ = '#000000'; */ Blockly.Block.prototype.dispose = function(healStack, animate) { this.unplug(healStack, false); + if (Blockly.Events.isEnabled() && !this.isShadow()) { + Blockly.Events.fire(new Blockly.Events.Delete(this)); + } + Blockly.Events.disable(); // This block is now at the top of the workspace. // Remove this block from the workspace's list of top-most blocks. @@ -197,6 +205,7 @@ Blockly.Block.prototype.dispose = function(healStack, animate) { } // Remove from block database. delete Blockly.Block.BlockDB_[this.id]; + Blockly.Events.enable(); }; /** @@ -483,6 +492,11 @@ Blockly.Block.prototype.setShadow = function(shadow) { return; // No change. } this.isShadow_ = shadow; + if (Blockly.Events.isEnabled() && !shadow) { + // Fire a creation event. + var xmlBlock = Blockly.Xml.blockToDom(this); + Blockly.Events.fire(new Blockly.Events.Create(this.workspace, xmlBlock)); + } }; /** @@ -737,11 +751,14 @@ Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { * @param {boolean} newBoolean True if inputs are horizontal. */ Blockly.Block.prototype.setInputsInline = function(newBoolean) { - this.inputsInline = newBoolean; - if (this.rendered) { - this.render(); - this.bumpNeighbours_(); - this.workspace.fireChangeEvent(); + if (this.inputsInline != newBoolean) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'inline', null, this.inputsInline, newBoolean)); + this.inputsInline = newBoolean; + if (this.rendered) { + this.render(); + this.bumpNeighbours_(); + } } }; @@ -777,7 +794,11 @@ Blockly.Block.prototype.getInputsInline = function() { * @param {boolean} disabled True if disabled. */ Blockly.Block.prototype.setDisabled = function(disabled) { - this.disabled = disabled; + if (this.disabled != disabled) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'disabled', null, this.disabled, disabled)); + this.disabled = disabled; + } }; /** @@ -812,6 +833,8 @@ Blockly.Block.prototype.isCollapsed = function() { */ Blockly.Block.prototype.setCollapsed = function(collapsed) { if (this.collapsed_ != collapsed) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'collapsed', null, this.collapsed_, collapsed)); this.collapsed_ = collapsed; } }; @@ -1207,6 +1230,8 @@ Blockly.Block.prototype.getCommentText = function() { */ Blockly.Block.prototype.setCommentText = function(text) { if (this.comment != text) { + Blockly.Events.fire(new Blockly.Events.Change( + this, 'comment', null, this.comment, text || '')); this.comment = text; } }; diff --git a/core/block_svg.js b/core/block_svg.js index 8bf962533..940568c85 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -227,7 +227,6 @@ Blockly.BlockSvg.terminateDrag_ = function() { selected.bumpNeighbours_, Blockly.BUMP_DELAY, selected); // Fire an event to allow scrollbars to resize. Blockly.fireUiEvent(window, 'resize'); - selected.workspace.fireChangeEvent(); } } Blockly.dragMode_ = 0; @@ -388,7 +387,6 @@ Blockly.BlockSvg.prototype.setCollapsed = function(collapsed) { // all their functions and store them next to each other. Expanding and // bumping causes all their definitions to go out of alignment. } - this.workspace.fireChangeEvent(); }; /** @@ -1089,11 +1087,8 @@ Blockly.BlockSvg.INNER_BOTTOM_LEFT_CORNER_HIGHLIGHT_LTR = * the next statement with the previous statement. Otherwise, dispose of * all children of this block. * @param {boolean} animate If true, show a disposal animation and sound. - * @param {boolean=} opt_dontRemoveFromWorkspace If true, don't remove this - * block from the workspace's list of top blocks. */ -Blockly.BlockSvg.prototype.dispose = function(healStack, animate, - opt_dontRemoveFromWorkspace) { +Blockly.BlockSvg.prototype.dispose = function(healStack, animate) { Blockly.Field.startCache(); // Terminate onchange event calls. if (this.onchangeWrapper_) { @@ -1116,12 +1111,14 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate, // Stop rerendering. this.rendered = false; + Blockly.BlockSvg.superClass_.dispose.call(this, healStack); + Blockly.Events.disable(); var icons = this.getIcons(); for (var i = 0; i < icons.length; i++) { icons[i].dispose(); } + Blockly.Events.enable(); - Blockly.BlockSvg.superClass_.dispose.call(this, healStack); goog.dom.removeNode(this.svgGroup_); // Sever JavaScript to DOM connections. @@ -1509,14 +1506,12 @@ Blockly.BlockSvg.prototype.setMutator = function(mutator) { * @param {boolean} disabled True if disabled. */ Blockly.BlockSvg.prototype.setDisabled = function(disabled) { - if (this.disabled == disabled) { - return; + if (this.disabled != disabled) { + Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled); + if (this.rendered) { + this.updateDisabled(); + } } - Blockly.BlockSvg.superClass_.setDisabled.call(this, disabled); - if (this.rendered) { - this.updateDisabled(); - } - this.workspace.fireChangeEvent(); }; /** diff --git a/core/comment.js b/core/comment.js index 4210d1df6..fd7816ac5 100644 --- a/core/comment.js +++ b/core/comment.js @@ -116,6 +116,14 @@ Blockly.Comment.prototype.createEditor_ = function() { Blockly.bindEvent_(this.textarea_, 'wheel', this, function(e) { e.stopPropagation(); }); + Blockly.bindEvent_(this.textarea_, 'change', this, function(e) { + if (this.text_ != this.textarea_.value) { + Blockly.Events.fire(new Blockly.Events.Change( + this.block_, 'comment', null, this.text_, this.textarea_.value)); + this.text_ = this.textarea_.value; + } + }); + return this.foreignObject_; }; @@ -243,6 +251,8 @@ Blockly.Comment.prototype.getText = function() { */ Blockly.Comment.prototype.setText = function(text) { if (this.text_ != text) { + Blockly.Events.fire(new Blockly.Events.Change( + this.block_, 'comment', null, this.text_, text)); this.text_ = text; } if (this.textarea_) { @@ -254,6 +264,9 @@ Blockly.Comment.prototype.setText = function(text) { * Dispose of this comment. */ Blockly.Comment.prototype.dispose = function() { + if (Blockly.Events.isEnabled()) { + this.setText(''); // Fire event to delete comment. + } this.block_.comment = null; Blockly.Icon.prototype.dispose.call(this); }; diff --git a/core/events.js b/core/events.js index 984ee51c0..1d0ac1df5 100644 --- a/core/events.js +++ b/core/events.js @@ -29,16 +29,39 @@ goog.provide('Blockly.Events'); /** * Allow change events to be created and fired. - * @type {boolean} + * @type {number} + * @private */ -Blockly.Events.enabled = true; +Blockly.Events.disabled_ = 0; + +/** + * Name of event that creates a block. + * @const + */ +Blockly.Events.CREATE = 'create'; + +/** + * Name of event that deletes a block. + * @const + */ +Blockly.Events.DELETE = 'delete'; + +/** + * Name of event that changes a block. + * @const + */ +Blockly.Events.CHANGE = 'change'; /** * Create a custom event and fire it. * @param {Object} detail Custom data for event. */ Blockly.Events.fire = function(detail) { - var workspace = Blockly.Workspace.getById(detail.workspace); + if (!Blockly.Events.isEnabled()) { + return; // No events allowed. + } + console.log(detail); + var workspace = Blockly.Workspace.getById(detail.workspaceId); if (workspace.rendered) { // Create a custom event in a browser-compatible way. if (typeof CustomEvent == 'function') { @@ -52,3 +75,91 @@ Blockly.Events.fire = function(detail) { workspace.getCanvas().dispatchEvent(evt); } }; + +/** + * Stop sending events. Every call to this function MUST also call enable. + */ +Blockly.Events.disable = function() { + Blockly.Events.disabled_++; +}; + +/** + * Start sending events. Unless events were already disabled when the + * corresponding call to disable was made. + */ +Blockly.Events.enable = function() { + Blockly.Events.disabled_--; +}; + +/** + * Returns whether events may be fired or not. + * @return {boolean} True if enabled. + */ +Blockly.Events.isEnabled = function() { + return Blockly.Events.disabled_ == 0; +}; + +/** + * Abstract class for a change event. + * @constructor + */ +Blockly.Events.Abstract = function() {}; + +/** + * Class for a block creation event. + * @param {!Blockly.Workspace} workspace The workspace. + * @param {!Element} xml XML DOM. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Create = function(workspace, xml) { + this.type = Blockly.Events.CREATE; + this.workspaceId = workspace.id; + this.xml = xml; +}; +goog.inherits(Blockly.Events.Create, Blockly.Events.Abstract); + +/** + * Class for a block deletion event. + * @param {!Blockly.Block} block The deleted block. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Delete = function(block) { + this.type = Blockly.Events.DELETE; + this.workspaceId = block.workspace.id; + this.blockId = block.id; + this.oldXml = Blockly.Xml.blockToDom(block); + var parent = block.getParent(); + if (parent) { + this.oldParentId = parent.id; + for (var i = 0, input; input = parent.inputList[i]; i++) { + if (input.connection && input.connection.targetBlock() == block) { + this.oldInput = input.name; + break; + } + } + } +}; +goog.inherits(Blockly.Events.Delete, Blockly.Events.Abstract); + +/** + * Class for a block change event. + * @param {!Blockly.Block} block The deleted block. + * @param {string} element One of 'field', 'comment', 'disabled', etc. + * @param {?string} name Name of input or field affected, or null. + * @param {string} oldValue Previous value of element. + * @param {string} newValue New value of element. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.Change = function(block, element, name, oldValue, newValue) { + this.type = Blockly.Events.CHANGE; + this.workspaceId = block.workspace.id; + this.blockId = block.id; + this.element = element; + this.name = name; + this.oldValue = oldValue; + this.newValue = newValue; +}; +goog.inherits(Blockly.Events.Create, Blockly.Events.Abstract); diff --git a/core/field.js b/core/field.js index 6891aa86d..84621fee4 100644 --- a/core/field.js +++ b/core/field.js @@ -42,7 +42,7 @@ goog.require('goog.userAgent'); */ Blockly.Field = function(text) { this.size_ = new goog.math.Size(0, 25); - this.setText(text); + this.setValue(text); }; /** @@ -139,6 +139,10 @@ Blockly.Field.prototype.init = function(block) { Blockly.bindEvent_(this.fieldGroup_, 'mouseup', this, this.onMouseUp_); // Force a render. this.updateTextNode_(); + if (Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, '', this.getValue())); + } }; /** @@ -326,7 +330,6 @@ Blockly.Field.prototype.setText = function(text) { if (this.sourceBlock_ && this.sourceBlock_.rendered) { this.sourceBlock_.render(); this.sourceBlock_.bumpNeighbours_(); - this.sourceBlock_.workspace.fireChangeEvent(); } }; @@ -386,6 +389,10 @@ Blockly.Field.prototype.setValue = function(newText) { if (oldText == newText) { return; } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, oldText, newText)); + } this.setText(newText); }; diff --git a/core/field_angle.js b/core/field_angle.js index 8fb311fcc..7495ff426 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -206,7 +206,7 @@ Blockly.FieldAngle.prototype.onMouseMove = function(e) { } angle = String(angle); Blockly.FieldTextInput.htmlInput_.value = angle; - this.setText(angle); + this.setValue(angle); this.validate_(); }; diff --git a/core/field_checkbox.js b/core/field_checkbox.js index c56b2e70e..a48a76215 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -91,9 +91,6 @@ Blockly.FieldCheckbox.prototype.setValue = function(strBool) { if (this.checkElement_) { this.checkElement_.style.display = newState ? 'block' : 'none'; } - if (this.sourceBlock_ && this.sourceBlock_.rendered) { - this.sourceBlock_.workspace.fireChangeEvent(); - } } }; diff --git a/core/field_colour.js b/core/field_colour.js index fe3b1f0e3..b1b58c519 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -45,11 +45,9 @@ goog.require('goog.ui.ColorPicker'); * @constructor */ Blockly.FieldColour = function(colour, opt_changeHandler) { - Blockly.FieldColour.superClass_.constructor.call(this, '\u00A0\u00A0\u00A0'); - + Blockly.FieldColour.superClass_.constructor.call(this, colour); + this.setText(Blockly.Field.NBSP + Blockly.Field.NBSP + Blockly.Field.NBSP); this.setChangeHandler(opt_changeHandler); - // Set the initial state. - this.setValue(colour); }; goog.inherits(Blockly.FieldColour, Blockly.Field); @@ -103,13 +101,15 @@ Blockly.FieldColour.prototype.getValue = function() { * @param {string} colour The new colour in '#rrggbb' format. */ Blockly.FieldColour.prototype.setValue = function(colour) { + if (this.sourceBlock_ && Blockly.Events.isEnabled() && + this.colour_ != colour) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.colour_, colour)); + } this.colour_ = colour; if (this.borderRect_) { this.borderRect_.style.fill = colour; } - if (this.sourceBlock_ && this.sourceBlock_.rendered) { - this.sourceBlock_.workspace.fireChangeEvent(); - } }; /** diff --git a/core/field_dropdown.js b/core/field_dropdown.js index fc8e4647c..76525ca99 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -54,10 +54,9 @@ Blockly.FieldDropdown = function(menuGenerator, opt_changeHandler) { this.setChangeHandler(opt_changeHandler); this.trimOptions_(); var firstTuple = this.getOptions_()[0]; - this.value_ = firstTuple[1]; // Call parent's constructor. - Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[0]); + Blockly.FieldDropdown.superClass_.constructor.call(this, firstTuple[1]); }; goog.inherits(Blockly.FieldDropdown, Blockly.Field); @@ -268,6 +267,10 @@ Blockly.FieldDropdown.prototype.setValue = function(newValue) { if (newValue === null || newValue === this.value_) { return; // No change if null. } + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.value_, newValue)); + } this.value_ = newValue; // Look up and display the human-readable text. var options = this.getOptions_(); @@ -311,7 +314,6 @@ Blockly.FieldDropdown.prototype.setText = function(text) { if (this.sourceBlock_ && this.sourceBlock_.rendered) { this.sourceBlock_.render(); this.sourceBlock_.bumpNeighbours_(); - this.sourceBlock_.workspace.fireChangeEvent(); } }; diff --git a/core/field_label.js b/core/field_label.js index da52f8d09..5e7a6cbb9 100644 --- a/core/field_label.js +++ b/core/field_label.js @@ -42,7 +42,7 @@ goog.require('goog.math.Size'); Blockly.FieldLabel = function(text, opt_class) { this.size_ = new goog.math.Size(0, 17.5); this.class_ = opt_class; - this.setText(text); + this.setValue(text); }; goog.inherits(Blockly.FieldLabel, Blockly.Field); diff --git a/core/field_textinput.js b/core/field_textinput.js index fd606609c..3891471eb 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -78,7 +78,7 @@ Blockly.FieldTextInput.prototype.dispose = function() { * @param {?string} text New text. * @override */ -Blockly.FieldTextInput.prototype.setText = function(text) { +Blockly.FieldTextInput.prototype.setValue = function(text) { if (text === null) { return; // No change if null. } @@ -90,7 +90,7 @@ Blockly.FieldTextInput.prototype.setText = function(text) { text = validated; } } - Blockly.Field.prototype.setText.call(this, text); + Blockly.Field.prototype.setValue.call(this, text); }; /** @@ -120,9 +120,7 @@ Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) { newValue = override; } } - if (newValue !== null) { - this.setText(newValue); - } + this.setValue(newValue); return; } @@ -172,7 +170,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { if (e.keyCode == enterKey) { Blockly.WidgetDiv.hide(); } else if (e.keyCode == escKey) { - this.setText(htmlInput.defaultValue); + htmlInput.value = htmlInput.defaultValue; Blockly.WidgetDiv.hide(); } else if (e.keyCode == tabKey) { Blockly.WidgetDiv.hide(); @@ -200,6 +198,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(e) { // Chrome only (version 26, OS X). this.sourceBlock_.render(); } + this.resizeEditor_(); }; /** @@ -275,7 +274,7 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { text = text1; } } - thisField.setText(text); + thisField.setValue(text); thisField.sourceBlock_.rendered && thisField.sourceBlock_.render(); Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_); Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_); diff --git a/core/field_variable.js b/core/field_variable.js index 4ab1d2895..d030e8c8b 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -87,14 +87,13 @@ Blockly.FieldVariable.prototype.init = function(block) { // Dropdown has already been initialized once. return; } - + Blockly.FieldVariable.superClass_.init.call(this, block); if (!this.getValue()) { // Variables without names get uniquely named for this workspace. var workspace = block.isInFlyout ? block.workspace.targetWorkspace : block.workspace; this.setValue(Blockly.Variables.generateUniqueName(workspace)); } - Blockly.FieldVariable.superClass_.init.call(this, block); }; /** @@ -110,6 +109,11 @@ Blockly.FieldVariable.prototype.getValue = function() { * Set the variable name. * @param {string} newValue New text. */ +Blockly.FieldVariable.prototype.setValue = function(newValue) { + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.Change( + this.sourceBlock_, 'field', this.name, this.value_, newValue)); + } this.value_ = newValue; this.setText(newValue); }; diff --git a/core/flyout.js b/core/flyout.js index 75b72687a..95153d5ec 100644 --- a/core/flyout.js +++ b/core/flyout.js @@ -455,7 +455,6 @@ Blockly.Flyout.prototype.show = function(xmlList) { Blockly.fireUiEventNow(window, 'resize'); this.reflowWrapper_ = Blockly.bindEvent_(this.workspace_.getCanvas(), 'blocklyWorkspaceChange', this, this.reflow); - this.workspace_.fireChangeEvent(); }; /** @@ -694,8 +693,8 @@ Blockly.Flyout.prototype.getRect = function() { } // Fix scale if nested in zoomed workspace. var scale = this.targetWorkspace_ == mainWorkspace ? 1 : mainWorkspace.scale; - return new goog.math.Rect(x, -BIG_NUM, - BIG_NUM + this.width_ * scale, BIG_NUM * 2); + return new goog.math.Rect(x, -BIG_NUM, + BIG_NUM + this.width_ * scale, BIG_NUM * 2); }; /** diff --git a/core/mutator.js b/core/mutator.js index 13d492a1e..430b58af0 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -296,8 +296,6 @@ Blockly.Mutator.prototype.workspaceChanged_ = function() { this.block_.render(); } this.resizeBubble_(); - // The source block may have changed, notify its workspace. - this.block_.workspace.fireChangeEvent(); goog.Timer.callOnce( this.block_.bumpNeighbours_, Blockly.BUMP_DELAY, this.block_); } diff --git a/core/workspace.js b/core/workspace.js index 400412a31..bfd123d3c 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -77,7 +77,6 @@ Blockly.Workspace.SCAN_ANGLE = 3; */ Blockly.Workspace.prototype.addTopBlock = function(block) { this.topBlocks_.push(block); - this.fireChangeEvent(); }; /** @@ -96,7 +95,6 @@ Blockly.Workspace.prototype.removeTopBlock = function(block) { if (!found) { throw 'Block not present in workspace\'s list of top-most blocks.'; } - this.fireChangeEvent(); }; /** @@ -193,13 +191,6 @@ Blockly.Workspace.prototype.remainingCapacity = function() { return this.options.maxBlocks - this.getAllBlocks().length; }; -/** - * Something on this workspace has changed. - */ -Blockly.Workspace.prototype.fireChangeEvent = function() { - // NOP. -}; - /** * Database of all workspaces. * @private diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 3d702e45f..7d03b44ed 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -463,20 +463,6 @@ Blockly.WorkspaceSvg.prototype.highlightBlock = function(id) { setTimeout(function() {thisWorkspace.traceOn(true);}, 1); }; -/** - * Fire a change event for this workspace. Changes include new block, dropdown - * edits, mutations, connections, etc. Groups of simultaneous changes (e.g. - * a tree of blocks being deleted) are merged into one event. - * Applications may hook workspace changes by listening for - * 'blocklyWorkspaceChange' on workspace.getCanvas(). - */ -Blockly.WorkspaceSvg.prototype.fireChangeEvent = function() { - if (this.rendered && this.svgBlockCanvas_) { - var details = {workspace: this.id}; - Blockly.Events.fire(details); - } -}; - /** * Paste the provided block onto the workspace. * @param {!Element} xmlBlock XML block element. @@ -691,7 +677,6 @@ Blockly.WorkspaceSvg.prototype.cleanUp_ = function() { } // Fire an event to allow scrollbars to resize. Blockly.fireUiEvent(window, 'resize'); - this.fireChangeEvent(); }; /** diff --git a/core/xml.js b/core/xml.js index efa24997d..1472ecc31 100644 --- a/core/xml.js +++ b/core/xml.js @@ -295,6 +295,7 @@ Blockly.Xml.domToWorkspace = function(workspace, xml) { */ Blockly.Xml.domToBlock = function(workspace, xmlBlock) { // Create top-level block. + Blockly.Events.disable(); var topBlock = Blockly.Xml.domToBlockHeadless_(workspace, xmlBlock); if (workspace.rendered) { // Hide connections to speed up assembly. @@ -319,6 +320,10 @@ Blockly.Xml.domToBlock = function(workspace, xmlBlock) { // Fire an event to allow scrollbars to resize. Blockly.fireUiEvent(window, 'resize'); } + Blockly.Events.enable(); + if (Blockly.Events.isEnabled() && !topBlock.isShadow()) { + Blockly.Events.fire(new Blockly.Events.Create(workspace, xmlBlock)); + } return topBlock; };