From 0122f36450e9692c814567804570a2dd1015c766 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Thu, 6 Jun 2019 08:39:01 -0700 Subject: [PATCH 01/13] Fixed field disposal, and reorganized editor creation. --- core/field.js | 25 +++--- core/field_angle.js | 115 ++++++++++++++------------- core/field_colour.js | 43 ++++------ core/field_date.js | 70 +++++++++-------- core/field_dropdown.js | 109 +++++++++++++------------- core/field_image.js | 7 +- core/field_label.js | 10 --- core/field_textinput.js | 168 +++++++++++++++++----------------------- 8 files changed, 258 insertions(+), 289 deletions(-) diff --git a/core/field.js b/core/field.js index a5302527f..f4b95e3da 100644 --- a/core/field.js +++ b/core/field.js @@ -111,7 +111,6 @@ Blockly.Field.cacheWidths_ = null; */ Blockly.Field.cacheReference_ = 0; - /** * Name of field. Unique within each block. * Static labels are usually unnamed. @@ -126,12 +125,12 @@ Blockly.Field.prototype.name = undefined; Blockly.Field.prototype.maxDisplayLength = 50; /** - * Get the current value of the field. - * @return {*} Current value. + * A generic value possessed by the field. + * Should generally be non-null, only null when the field is created. + * @type {*} + * @protected */ -Blockly.Field.prototype.getValue = function() { - return this.value_; -}; +Blockly.Field.prototype.value_ = null; /** * Visible text to display. @@ -329,8 +328,12 @@ Blockly.Field.prototype.toXml = function(fieldElement) { /** * Dispose of all DOM objects belonging to this editable field. + * @package */ Blockly.Field.prototype.dispose = function() { + Blockly.DropDownDiv.hideIfOwner(this); + Blockly.WidgetDiv.hideIfOwner(this); + if (this.mouseDownWrapper_) { Blockly.unbindEvent_(this.mouseDownWrapper_); this.mouseDownWrapper_ = null; @@ -785,12 +788,12 @@ Blockly.Field.prototype.setValue = function(newValue) { }; /** - * A generic value possessed by the field. - * Should generally be non-null, only null when the field is created. - * @type {*} - * @protected + * Get the current value of the field. + * @return {*} Current value. */ -Blockly.Field.prototype.value_ = null; +Blockly.Field.prototype.getValue = function() { + return this.value_; +}; /** * Used to validate a value. Returns input by default. Can be overridden by diff --git a/core/field_angle.js b/core/field_angle.js index 36ca268dc..a3c3574d3 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -130,6 +130,15 @@ Blockly.FieldAngle.prototype.initView = function() { this.textElement_.appendChild(this.symbol_); }; +/** + * Dispose of references to DOM elements and events belonging to this field. + * @package + */ +Blockly.FieldAngle.prototype.dispose = function() { + Blockly.FieldAngle.superClass_.dispose.call(this); + this.symbol_ = null; +}; + /** * Updates the graph when the field rerenders. * @private @@ -140,58 +149,55 @@ Blockly.FieldAngle.prototype.render_ = function() { }; /** - * Clean up this FieldAngle, as well as the inherited FieldTextInput. - * @return {!Function} Closure to call on destruction of the WidgetDiv. - * @private - */ -Blockly.FieldAngle.prototype.dispose_ = function() { - var thisField = this; - return function() { - Blockly.FieldAngle.superClass_.dispose_.call(thisField)(); - thisField.gauge_ = null; - if (thisField.clickWrapper_) { - Blockly.unbindEvent_(thisField.clickWrapper_); - } - if (thisField.moveWrapper1_) { - Blockly.unbindEvent_(thisField.moveWrapper1_); - } - if (thisField.moveWrapper2_) { - Blockly.unbindEvent_(thisField.moveWrapper2_); - } - }; -}; - -/** - * Show the inline free-text editor on top of the text. + * Create and show the angle field's editor. * @private */ Blockly.FieldAngle.prototype.showEditor_ = function() { - var noFocus = - Blockly.utils.userAgent.MOBILE || - Blockly.utils.userAgent.ANDROID || - Blockly.utils.userAgent.IPAD; // Mobile browsers have issues with in-line textareas (focus & keyboards). + var noFocus = + Blockly.userAgent.MOBILE || + Blockly.userAgent.ANDROID || + Blockly.userAgent.IPAD; Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); - var div = Blockly.DropDownDiv.getContentDiv(); + var editor = this.dropdownCreate_(); + Blockly.DropDownDiv.getContentDiv().appendChild(editor); - // Build the SVG DOM. - var svg = Blockly.utils.dom.createSvgElement('svg', { - 'xmlns': Blockly.utils.dom.SVG_NS, - 'xmlns:html': Blockly.utils.dom.HTML_NS, - 'xmlns:xlink': Blockly.utils.dom.XLINK_NS, + var border = this.sourceBlock_.getColourBorder(); + border = border.colourBorder == null ? + border.colourLight : border.colourBorder; + Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), border); + + Blockly.DropDownDiv.showPositionedByField( + this, this.dropdownDispose_.bind(this)); + + this.updateGraph_(); +}; + +/** + * Create the angle dropdown editor. + * @return {!Element} The newly created angle picker. + * @private + */ +Blockly.FieldAngle.prototype.dropdownCreate_ = function() { + var svg = Blockly.utils.createSvgElement('svg', { + 'xmlns': 'http://www.w3.org/2000/svg', + 'xmlns:html': 'http://www.w3.org/1999/xhtml', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'version': '1.1', 'height': (Blockly.FieldAngle.HALF * 2) + 'px', 'width': (Blockly.FieldAngle.HALF * 2) + 'px' - }, div); - var circle = Blockly.utils.dom.createSvgElement('circle', { - 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, + }, null); + var circle = Blockly.utils.createSvgElement('circle', { + 'cx': Blockly.FieldAngle.HALF, + 'cy': Blockly.FieldAngle.HALF, 'r': Blockly.FieldAngle.RADIUS, 'class': 'blocklyAngleCircle' }, svg); - this.gauge_ = Blockly.utils.dom.createSvgElement('path', - {'class': 'blocklyAngleGauge'}, svg); - this.line_ = Blockly.utils.dom.createSvgElement('line', { + this.gauge_ = Blockly.utils.createSvgElement('path', { + 'class': 'blocklyAngleGauge' + }, svg); + this.line_ = Blockly.utils.createSvgElement('line', { 'x1': Blockly.FieldAngle.HALF, 'y1': Blockly.FieldAngle.HALF, 'class': 'blocklyAngleLine' @@ -210,33 +216,38 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { }, svg); } - var border = this.sourceBlock_.getColourBorder(); - border = (border.colourBorder == null) ? - border.colourLight : border.colourBorder; - - Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), border); - Blockly.DropDownDiv.showPositionedByField(this); // The angle picker is different from other fields in that it updates on // mousemove even if it's not in the middle of a drag. In future we may // change this behaviour. For now, using bindEvent_ instead of // bindEventWithChecks_ allows it to work without a mousedown/touchstart. this.clickWrapper_ = - Blockly.bindEvent_(svg, 'click', this, this.hide_.bind(this)); + Blockly.bindEvent_(svg, 'click', this, this.hide_); this.moveWrapper1_ = Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); this.moveWrapper2_ = Blockly.bindEvent_(this.gauge_, 'mousemove', this, this.onMouseMove); - this.updateGraph_(); + + return svg; }; /** - * Hide the editor and unbind event listeners. + * Dispose of references to DOM elements and events belonging + * to the angle editor. + * @private + */ +Blockly.FieldAngle.prototype.dropdownDispose_ = function() { + this.gauge_ = null; + this.line_ = null; + Blockly.unbindEvent_(this.clickWrapper_); + Blockly.unbindEvent_(this.moveWrapper1_); + Blockly.unbindEvent_(this.moveWrapper2_); +}; + +/** + * Hide the editor. * @private */ Blockly.FieldAngle.prototype.hide_ = function() { - Blockly.unbindEvent_(this.moveWrapper1_); - Blockly.unbindEvent_(this.moveWrapper2_); - Blockly.unbindEvent_(this.clickWrapper_); Blockly.DropDownDiv.hideIfOwner(this); Blockly.WidgetDiv.hide(); }; @@ -287,7 +298,7 @@ Blockly.FieldAngle.prototype.onMouseMove = function(e) { // Update value. var angleString = String(angle); if (angleString != this.text_) { - Blockly.FieldTextInput.htmlInput_.value = angle; + this.htmlInput_.value = angle; this.setValue(angle); // Always render the input angle. this.text_ = angleString; diff --git a/core/field_colour.js b/core/field_colour.js index fb0641878..894f642f9 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -154,14 +154,6 @@ Blockly.FieldColour.prototype.initView = function() { this.borderRect_.style.fill = this.value_; }; -/** - * Close the colour picker if this input is being deleted. - */ -Blockly.FieldColour.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldColour.superClass_.dispose.call(this); -}; - /** * Ensure that the input value is a valid colour. * @param {string=} newValue The input value. @@ -271,20 +263,18 @@ Blockly.FieldColour.prototype.setColumns = function(columns) { }; /** - * Create a palette under the colour field. + * Create and show the colour field's editor. * @private */ Blockly.FieldColour.prototype.showEditor_ = function() { - var picker = this.createWidget_(); + var picker = this.dropdownCreate_(); Blockly.DropDownDiv.getContentDiv().appendChild(picker); + Blockly.DropDownDiv.setColour( this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR); - Blockly.DropDownDiv.showPositionedByField(this); - - // Configure event handler on the table to listen for any event in a cell. - Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker, - 'mouseup', this, this.onClick_); + Blockly.DropDownDiv.showPositionedByField( + this, this.dropdownDispose_.bind(this)); }; /** @@ -299,22 +289,17 @@ Blockly.FieldColour.prototype.onClick_ = function(e) { cell = cell.parentNode; } var colour = cell && cell.label; - Blockly.WidgetDiv.hide(); - if (this.sourceBlock_) { - // Call any validation function, and allow it to override. - colour = this.callValidator(colour); - } if (colour !== null) { this.setValue(colour); } }; /** - * Create a colour picker widget. + * Create a colour picker dropdown editor. * @return {!Element} The newly created colour picker. * @private */ -Blockly.FieldColour.prototype.createWidget_ = function() { +Blockly.FieldColour.prototype.dropdownCreate_ = function() { var columns = this.columns_ || Blockly.FieldColour.COLUMNS; var colours = this.colours_ || Blockly.FieldColour.COLOURS; var titles = this.titles_ || Blockly.FieldColour.TITLES; @@ -339,18 +324,20 @@ Blockly.FieldColour.prototype.createWidget_ = function() { div.className = 'blocklyColourSelected'; } } + + // Configure event handler on the table to listen for any event in a cell. + this.onUpWrapper_ = Blockly.bindEvent_(table, 'mouseup', this, this.onClick_); + return table; }; /** - * Hide the colour picker widget. + * Dispose of references to DOM elements and events belonging + * to the colour editor. * @private */ -Blockly.FieldColour.widgetDispose_ = function() { - if (Blockly.FieldColour.onUpWrapper_) { - Blockly.unbindEvent_(Blockly.FieldColour.onUpWrapper_); - } - Blockly.Events.setGroup(false); +Blockly.FieldColour.prototype.dropdownDispose_ = function() { + Blockly.unbindEvent_(this.onUpWrapper_); }; Blockly.Field.register('field_colour', Blockly.FieldColour); diff --git a/core/field_date.js b/core/field_date.js index 0878f78b1..843d53562 100644 --- a/core/field_date.js +++ b/core/field_date.js @@ -99,14 +99,6 @@ Blockly.FieldDate.prototype.DROPDOWN_BORDER_COLOUR = 'silver'; */ Blockly.FieldDate.prototype.DROPDOWN_BACKGROUND_COLOUR = 'white'; -/** - * Close the colour picker if this input is being deleted. - */ -Blockly.FieldDate.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldDate.superClass_.dispose.call(this); -}; - /** * Ensure that the input value is a valid date. * @param {string=} newValue The input value. @@ -195,38 +187,28 @@ Blockly.FieldDate.prototype.updateEditor_ = function() { }; /** - * Create a date picker under the date field. + * Create and show the date field's editor. * @private */ Blockly.FieldDate.prototype.showEditor_ = function() { - this.picker_ = this.createWidget_(); + this.picker_ = this.dropdownCreate_(); this.picker_.render(Blockly.DropDownDiv.getContentDiv()); Blockly.utils.dom.addClass(this.picker_.getElement(), 'blocklyDatePicker'); - Blockly.DropDownDiv.showPositionedByField(this); Blockly.DropDownDiv.setColour( this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR); - this.updateEditor_(); - var thisField = this; - Blockly.FieldDate.changeEventKey_ = goog.events.listen(this.picker_, - goog.ui.DatePicker.Events.CHANGE, - function(event) { - var date = event.date ? event.date.toIsoString(true) : ''; - thisField.setValue(date); - }); - Blockly.FieldDate.changeEventKey_ = goog.events.listen(this.picker_, - goog.ui.DatePicker.Events.CHANGE_ACTIVE_MONTH, - function(_e) { - thisField.updateEditor_(); - }); + Blockly.DropDownDiv.showPositionedByField( + this, this.dropdownDispose_.bind(this)); + + this.updateEditor_(); }; /** - * Create a date picker widget and render it inside the widget div. + * Create the date dropdown editor. * @return {!goog.ui.DatePicker} The newly created date picker. * @private */ -Blockly.FieldDate.prototype.createWidget_ = function() { +Blockly.FieldDate.prototype.dropdownCreate_ = function() { // Create the date picker using Closure. Blockly.FieldDate.loadLanguage_(); var picker = new goog.ui.DatePicker(); @@ -235,19 +217,43 @@ Blockly.FieldDate.prototype.createWidget_ = function() { picker.setUseNarrowWeekdayNames(true); picker.setUseSimpleNavigationMenu(true); picker.setDate(goog.date.DateTime.fromIsoString(this.getValue())); + + this.changeEventKey_ = goog.events.listen( + picker, + goog.ui.DatePicker.Events.CHANGE, + this.onDateSelected_, + null, + this); + this.activeMonthEventKey_ = goog.events.listen( + picker, + goog.ui.DatePicker.Events.CHANGE_ACTIVE_MONTH, + this.updateEditor_, + null, + this); + return picker; }; /** - * Hide the date picker. + * Dispose of references to DOM elements and events belonging + * to the date editor. * @private */ -Blockly.FieldDate.widgetDispose_ = function() { - if (Blockly.FieldDate.changeEventKey_) { - goog.events.unlistenByKey(Blockly.FieldDate.changeEventKey_); - } +Blockly.FieldDate.prototype.dropdownDispose_ = function() { this.picker_ = null; - Blockly.Events.setGroup(false); + goog.events.unlistenByKey(this.changeEventKey_); + goog.events.unlistenByKey(this.activeMonthEventKey_); +}; + +/** + * Handle a CHANGE event in the date picker. + * TODO: Not sure what the type for goog event information is. + * @param {!Event} event The CHANGE event. + * @private + */ +Blockly.FieldDate.prototype.onDateSelected_ = function(event) { + var date = event.date ? event.date.toIsoString(true) : ''; + this.setValue(date); }; /** diff --git a/core/field_dropdown.js b/core/field_dropdown.js index be5ccfaad..5b1272f2d 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -154,47 +154,46 @@ Blockly.FieldDropdown.prototype.initView = function() { } }; +/** + * Dispose of references to DOM elements and events belonging to this field. + */ +Blockly.FieldDropdown.prototype.dispose = function() { + Blockly.FieldDropdown.superClass_.dispose.call(this); + this.imageElement_ = null; + this.arrow_ = null; +}; + /** * Create a dropdown menu under the text. * @private */ Blockly.FieldDropdown.prototype.showEditor_ = function() { - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, null); - var menu = this.createMenu_(); - this.addActionListener_(menu); - this.positionMenu_(menu); + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, + this.widgetDispose_.bind(this)); + this.menu_ = this.widgetCreate_(); + + this.menu_.render(Blockly.WidgetDiv.DIV); + // Element gets created in render. + Blockly.utils.addClass(this.menu_.getElement(), 'blocklyDropdownMenu'); + + this.positionMenu_(this.menu_); + + // Focusing needs to be handled after the menu is rendered and positioned. + // Otherwise it will cause a page scroll to get the misplaced menu in + // view. See issue #1329. + this.menu_.setAllowAutoFocus(true); + this.menu_.getElement().focus(); }; /** - * Add a listener for mouse and keyboard events in the menu and its items. - * @param {!goog.ui.Menu} menu The menu to add listeners to. + * Create the dropdown editor widget. + * @return {goog.ui.Menu} The newly created dropdown menu. * @private */ -Blockly.FieldDropdown.prototype.addActionListener_ = function(menu) { - var thisField = this; - - function callback(e) { - var menu = this; - var menuItem = e.target; - if (menuItem) { - thisField.onItemSelected(menu, menuItem); - } - Blockly.WidgetDiv.hideIfOwner(thisField); - Blockly.Events.setGroup(false); - } - // Listen for mouse/keyboard events. - goog.events.listen(menu, goog.ui.Component.EventType.ACTION, callback); -}; - -/** - * Create and populate the menu and menu items for this dropdown, based on - * the options list. - * @return {!goog.ui.Menu} The populated dropdown menu. - * @private - */ -Blockly.FieldDropdown.prototype.createMenu_ = function() { +Blockly.FieldDropdown.prototype.widgetCreate_ = function() { var menu = new goog.ui.Menu(); menu.setRightToLeft(this.sourceBlock_.RTL); + var options = this.getOptions(); for (var i = 0; i < options.length; i++) { var content = options[i][0]; // Human-readable text or image. @@ -213,9 +212,33 @@ Blockly.FieldDropdown.prototype.createMenu_ = function() { menu.addChild(menuItem, true); menuItem.setChecked(value == this.value_); } + + this.menuActionEventKey_ = goog.events.listen( + menu, + goog.ui.Component.EventType.ACTION, + this.handleMenuActionEvent_, + false, + this); + return menu; }; +Blockly.FieldDropdown.prototype.widgetDispose_ = function() { + this.menu_ = null; + goog.events.unlistenByKey(this.menuActionEventKey_); +}; + +/** + * Handle an ACTION event in the dropdown menu. + * TODO: Not sure what the type for goog event information is. + * @param {!Event} event The CHANGE event. + * @private + */ +Blockly.FieldDropdown.prototype.handleMenuActionEvent_ = function(event) { + Blockly.WidgetDiv.hideIfOwner(this); + this.onItemSelected(this.menu_, event.target); +}; + /** * Place the menu correctly on the screen, taking into account the dimensions * of the menu and the dimensions of the screen so that it doesn't run off any @@ -224,11 +247,9 @@ Blockly.FieldDropdown.prototype.createMenu_ = function() { * @private */ Blockly.FieldDropdown.prototype.positionMenu_ = function(menu) { - // Record viewport dimensions before adding the dropdown. var viewportBBox = Blockly.utils.getViewportBBox(); var anchorBBox = this.getAnchorDimensions_(); - this.createWidget_(menu); var menuSize = Blockly.utils.uiMenu.getSize(menu); var menuMaxHeightPx = Blockly.FieldDropdown.MAX_MENU_HEIGHT_VH @@ -240,26 +261,8 @@ Blockly.FieldDropdown.prototype.positionMenu_ = function(menu) { if (this.sourceBlock_.RTL) { Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize); } - // Position the menu. Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, this.sourceBlock_.RTL); - // Calling menuDom.focus() has to wait until after the menu has been placed - // correctly. Otherwise it will cause a page scroll to get the misplaced menu - // in view. See issue #1329. - menu.getElement().focus(); -}; - -/** - * Create and render the menu widget inside Blockly's widget div. - * @param {!goog.ui.Menu} menu The menu to add to the widget div. - * @private - */ -Blockly.FieldDropdown.prototype.createWidget_ = function(menu) { - var div = Blockly.WidgetDiv.DIV; - menu.render(div); - Blockly.utils.dom.addClass(menu.getElement(), 'blocklyDropdownMenu'); - // Enable autofocus after the initial render to avoid issue #1329. - menu.setAllowAutoFocus(true); }; /** @@ -508,14 +511,6 @@ Blockly.FieldDropdown.prototype.renderSelectedText_ = function() { this.size_.width = Blockly.Field.getCachedWidth(this.textElement_); }; -/** - * Close the dropdown menu if this input is being deleted. - */ -Blockly.FieldDropdown.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldDropdown.superClass_.dispose.call(this); -}; - /** * Validates the data structure to be processed as an options list. * @param {?} options The proposed dropdown options. diff --git a/core/field_image.js b/core/field_image.js index cc4ba0617..7a7bb96ec 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -133,11 +133,10 @@ Blockly.FieldImage.prototype.initView = function() { * Dispose of all DOM objects belonging to this text. */ Blockly.FieldImage.prototype.dispose = function() { - if (this.fieldGroup_) { - Blockly.utils.dom.removeNode(this.fieldGroup_); - this.fieldGroup_ = null; - } + Blockly.FieldImage.superClass_.dispose.call(this); this.imageElement_ = null; + // TODO: Do we need to dispose of this? + this.clickHandler_ = null; }; /** diff --git a/core/field_label.js b/core/field_label.js index 00cb8a35d..efeeb1e6c 100644 --- a/core/field_label.js +++ b/core/field_label.js @@ -87,16 +87,6 @@ Blockly.FieldLabel.prototype.initView = function() { } }; -/** - * Dispose of all DOM objects belonging to this text. - */ -Blockly.FieldLabel.prototype.dispose = function() { - if (this.textElement_) { - Blockly.utils.dom.removeNode(this.textElement_); - this.textElement_ = null; - } -}; - /** * Ensure that the input value casts to a valid string. * @param {string=} newValue The input value. diff --git a/core/field_textinput.js b/core/field_textinput.js index 027f4f683..87a43b75a 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -107,14 +107,6 @@ Blockly.FieldTextInput.prototype.CURSOR = 'text'; */ Blockly.FieldTextInput.prototype.spellcheck_ = true; -/** - * Close the input widget if this input is being deleted. - */ -Blockly.FieldTextInput.prototype.dispose = function() { - Blockly.WidgetDiv.hideIfOwner(this); - Blockly.FieldTextInput.superClass_.dispose.call(this); -}; - /** * Ensure that the input value casts to a valid string. * @param {string=} newValue The input value. @@ -137,15 +129,12 @@ Blockly.FieldTextInput.prototype.doClassValidation_ = function(newValue) { Blockly.FieldTextInput.prototype.doValueInvalid_ = function() { if (this.isBeingEdited_) { this.isTextValid_ = false; - var htmlInput = Blockly.FieldTextInput.htmlInput_; - if (htmlInput) { - var oldValue = this.value_; - // Revert value when the text becomes invalid. - this.value_ = htmlInput.untypedDefaultValue_; - if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.BlockChange( - this.sourceBlock_, 'field', this.name, oldValue, this.value_)); - } + var oldValue = this.value_; + // Revert value when the text becomes invalid. + this.value_ = this.htmlInput_.untypedDefaultValue_; + if (this.sourceBlock_ && Blockly.Events.isEnabled()) { + Blockly.Events.fire(new Blockly.Events.BlockChange( + this.sourceBlock_, 'field', this.name, oldValue, this.value_)); } } }; @@ -177,12 +166,11 @@ Blockly.FieldTextInput.prototype.render_ = function() { // This logic is done in render_ rather than doValueInvalid_ or // doValueUpdate_ so that the code is more centralized. if (this.isBeingEdited_) { - var htmlInput = Blockly.FieldTextInput.htmlInput_; this.resizeEditor_(); if (!this.isTextValid_) { - Blockly.utils.dom.addClass(htmlInput, 'blocklyInvalidInput'); + Blockly.utils.dom.addClass(this.htmlInput_, 'blocklyInvalidInput'); } else { - Blockly.utils.dom.removeClass(htmlInput, 'blocklyInvalidInput'); + Blockly.utils.dom.removeClass(this.htmlInput_, 'blocklyInvalidInput'); } } }; @@ -233,76 +221,74 @@ Blockly.FieldTextInput.prototype.showPromptEditor_ = function() { * @private */ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { - // Start editing. this.isBeingEdited_ = true; + Blockly.WidgetDiv.show( + this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this)); + this.htmlInput_ = this.widgetCreate_(); - // Show the div. - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_()); + if (!quietInput) { + this.htmlInput_.focus(); + this.htmlInput_.select(); + } + + this.bindInputEvents_(); +}; + +Blockly.FieldTextInput.prototype.widgetCreate_ = function() { var div = Blockly.WidgetDiv.DIV; - // Create the input. var htmlInput = document.createElement('input'); htmlInput.className = 'blocklyHtmlInput'; htmlInput.setAttribute('spellcheck', this.spellcheck_); var fontSize = - (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; + (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; div.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; div.appendChild(htmlInput); - // Assign the current value to the input & resize. htmlInput.value = htmlInput.defaultValue = this.value_; htmlInput.untypedDefaultValue_ = this.value_; htmlInput.oldValue_ = null; this.resizeEditor_(); - // Give it focus. - if (!quietInput) { - htmlInput.focus(); - htmlInput.select(); - } - - // Bind events. this.bindInputEvents_(htmlInput); - // Save it. - Blockly.FieldTextInput.htmlInput_ = htmlInput; + return htmlInput; }; /** - * Bind handlers for user input on this field and size changes on the workspace. - * @param {!HTMLInputElement} htmlInput The htmlInput created in showEditor, to - * which event handlers will be bound. + * Bind handlers for user input on the text input field's editor.. * @private */ -Blockly.FieldTextInput.prototype.bindInputEvents_ = function(htmlInput) { - // Bind to keydown -- trap Enter without IME and Esc to hide. - htmlInput.onKeyDownWrapper_ = +Blockly.FieldTextInput.prototype.bindInputEvents_ = function() { + // Trap Enter without IME and Esc to hide. + this.onKeyDownWrapper_ = Blockly.bindEventWithChecks_( - htmlInput, 'keydown', this, this.onHtmlInputKeyDown_); - // Bind to keyup -- resize after every keystroke. - htmlInput.onKeyUpWrapper_ = + this.htmlInput_, 'keydown', this, this.onHtmlInputKeyDown_); + // Resize after every keystroke. + this.onKeyUpWrapper_ = Blockly.bindEventWithChecks_( - htmlInput, 'keyup', this, this.onHtmlInputChange_); - // Bind to keyPress -- repeatedly resize when holding down a key. - htmlInput.onKeyPressWrapper_ = + this.htmlInput_, 'keyup', this, this.onHtmlInputChange_); + // Repeatedly resize when holding down a key. + this.onKeyPressWrapper_ = Blockly.bindEventWithChecks_( - htmlInput, 'keypress', this, this.onHtmlInputChange_); - htmlInput.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this); - this.workspace_.addChangeListener(htmlInput.onWorkspaceChangeWrapper_); + this.htmlInput_, 'keypress', this, this.onHtmlInputChange_); + + // TODO: Figure out if this is necessary. + this.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this); + this.workspace_.addChangeListener(this.onWorkspaceChangeWrapper_); }; /** * Unbind handlers for user input and workspace size changes. - * @param {!HTMLInputElement} htmlInput The html for this text input. * @private */ -Blockly.FieldTextInput.prototype.unbindInputEvents_ = function(htmlInput) { - Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_); - Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_); - Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_); +Blockly.FieldTextInput.prototype.unbindInputEvents_ = function() { + Blockly.unbindEvent_(this.onKeyDownWrapper_); + Blockly.unbindEvent_(this.onKeyUpWrapper_); + Blockly.unbindEvent_(this.onKeyPressWrapper_); this.workspace_.removeChangeListener( - htmlInput.onWorkspaceChangeWrapper_); + this.onWorkspaceChangeWrapper_); }; /** @@ -311,13 +297,12 @@ Blockly.FieldTextInput.prototype.unbindInputEvents_ = function(htmlInput) { * @private */ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { - var htmlInput = Blockly.FieldTextInput.htmlInput_; var tabKey = 9, enterKey = 13, escKey = 27; if (e.keyCode == enterKey) { Blockly.WidgetDiv.hide(); Blockly.DropDownDiv.hideIfOwner(this); } else if (e.keyCode == escKey) { - htmlInput.value = htmlInput.defaultValue; + this.htmlInput_.value = this.htmlInput_.defaultValue; Blockly.WidgetDiv.hide(); Blockly.DropDownDiv.hideIfOwner(this); } else if (e.keyCode == tabKey) { @@ -334,10 +319,9 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { * @private */ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) { - var htmlInput = Blockly.FieldTextInput.htmlInput_; - var text = htmlInput.value; - if (text !== htmlInput.oldValue_) { - htmlInput.oldValue_ = text; + var text = this.htmlInput_.value; + if (text !== this.htmlInput_.oldValue_) { + this.htmlInput_.oldValue_ = text; // TODO(#2169): Once issue is fixed the setGroup functionality could be // moved up to the Field setValue method. This would create a @@ -345,7 +329,7 @@ Blockly.FieldTextInput.prototype.onHtmlInputChange_ = function(_e) { Blockly.Events.setGroup(true); this.setValue(text); // Always render the input text. - this.text_ = Blockly.FieldTextInput.htmlInput_.value; + this.text_ = this.htmlInput_.value; this.forceRerender(); Blockly.Events.setGroup(false); } @@ -384,44 +368,38 @@ Blockly.FieldTextInput.prototype.resizeEditor_ = function() { /** * Close the editor, save the results, and dispose of the editable * text field's elements. - * @return {!Function} Closure to call on destruction of the WidgetDiv. * @private */ Blockly.FieldTextInput.prototype.widgetDispose_ = function() { - var thisField = this; - return function() { - var htmlInput = Blockly.FieldTextInput.htmlInput_; + // Finalize value. + this.isBeingEdited_ = false; + // No need to call setValue because if the widget is being closed the + // latest input text has already been validated. + if (this.value_ !== this.text_) { + // At the end of an edit the text should be the same as the value. It + // may not be if the input text is different than the validated text. + // We should fix that. + this.text_ = String(this.value_); + this.isTextValid_ = true; + this.forceRerender(); + } + // Otherwise don't rerender. - // Finalize value. - thisField.isBeingEdited_ = false; - // No need to call setValue because if the widget is being closed the - // latest input text has already been validated. - if (thisField.value_ !== thisField.text_) { - // At the end of an edit the text should be the same as the value. It - // may not be if the input text is different than the validated text. - // We should fix that. - thisField.text_ = String(thisField.value_); - thisField.isTextValid_ = true; - thisField.forceRerender(); - } - // Otherwise don't rerender. + // Call onFinishEditing + // TODO: Get rid of this or make it less of a hack. + if (this.onFinishEditing_) { + this.onFinishEditing_(this.value_); + } - // Call onFinishEditing - // TODO: Get rid of this or make it less of a hack. - if (thisField.onFinishEditing_) { - thisField.onFinishEditing_(thisField.value_); - } + // Remove htmlInput. + this.unbindInputEvents_(); + this.htmlInput_ = null; - // Remove htmlInput. - thisField.unbindInputEvents_(htmlInput); - Blockly.FieldTextInput.htmlInput_ = null; - - // Delete style properties. - var style = Blockly.WidgetDiv.DIV.style; - style.width = 'auto'; - style.height = 'auto'; - style.fontSize = ''; - }; + // Delete style properties. + var style = Blockly.WidgetDiv.DIV.style; + style.width = 'auto'; + style.height = 'auto'; + style.fontSize = ''; }; /** From 217206911a92f7a0c657cd55d7cef309a271922f Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 7 Jun 2019 07:03:43 -0700 Subject: [PATCH 02/13] Fixed some formatting in text input. --- core/field_textinput.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/field_textinput.js b/core/field_textinput.js index 87a43b75a..20636c8e8 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -230,8 +230,6 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { this.htmlInput_.focus(); this.htmlInput_.select(); } - - this.bindInputEvents_(); }; Blockly.FieldTextInput.prototype.widgetCreate_ = function() { @@ -257,22 +255,24 @@ Blockly.FieldTextInput.prototype.widgetCreate_ = function() { }; /** - * Bind handlers for user input on the text input field's editor.. + * Bind handlers for user input on the text input field's editor. + * @param {!HTMLInputElement} htmlInput The htmlInput to which event + * handlers will be bound. * @private */ -Blockly.FieldTextInput.prototype.bindInputEvents_ = function() { +Blockly.FieldTextInput.prototype.bindInputEvents_ = function(htmlInput) { // Trap Enter without IME and Esc to hide. this.onKeyDownWrapper_ = Blockly.bindEventWithChecks_( - this.htmlInput_, 'keydown', this, this.onHtmlInputKeyDown_); + htmlInput, 'keydown', this, this.onHtmlInputKeyDown_); // Resize after every keystroke. this.onKeyUpWrapper_ = Blockly.bindEventWithChecks_( - this.htmlInput_, 'keyup', this, this.onHtmlInputChange_); + htmlInput, 'keyup', this, this.onHtmlInputChange_); // Repeatedly resize when holding down a key. this.onKeyPressWrapper_ = Blockly.bindEventWithChecks_( - this.htmlInput_, 'keypress', this, this.onHtmlInputChange_); + htmlInput, 'keypress', this, this.onHtmlInputChange_); // TODO: Figure out if this is necessary. this.onWorkspaceChangeWrapper_ = this.resizeEditor_.bind(this); From 303115953e195742927efdc1262d3a46d18f3c63 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 7 Jun 2019 07:16:52 -0700 Subject: [PATCH 03/13] Moved to #2125 disposal. Cleaned up some doc. --- core/field.js | 15 +++----- core/field_angle.js | 14 +------- core/field_colour.js | 3 +- core/field_date.js | 1 - core/field_dropdown.js | 14 +++----- core/field_image.js | 10 ------ core/field_textinput.js | 78 ++++++++++++++++++++++------------------- core/field_variable.js | 10 ------ 8 files changed, 52 insertions(+), 93 deletions(-) diff --git a/core/field.js b/core/field.js index f4b95e3da..ecd8469cb 100644 --- a/core/field.js +++ b/core/field.js @@ -327,7 +327,7 @@ Blockly.Field.prototype.toXml = function(fieldElement) { }; /** - * Dispose of all DOM objects belonging to this editable field. + * Dispose of all DOM objects and events belonging to this editable field. * @package */ Blockly.Field.prototype.dispose = function() { @@ -336,16 +336,11 @@ Blockly.Field.prototype.dispose = function() { if (this.mouseDownWrapper_) { Blockly.unbindEvent_(this.mouseDownWrapper_); - this.mouseDownWrapper_ = null; } - this.sourceBlock_ = null; - if (this.fieldGroup_) { - Blockly.utils.dom.removeNode(this.fieldGroup_); - this.fieldGroup_ = null; - } - this.textElement_ = null; - this.borderRect_ = null; - this.validator_ = null; + + Blockly.utils.dom.removeNode(this.fieldGroup_); + + this.disposed = true; }; /** diff --git a/core/field_angle.js b/core/field_angle.js index a3c3574d3..13e264216 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -130,15 +130,6 @@ Blockly.FieldAngle.prototype.initView = function() { this.textElement_.appendChild(this.symbol_); }; -/** - * Dispose of references to DOM elements and events belonging to this field. - * @package - */ -Blockly.FieldAngle.prototype.dispose = function() { - Blockly.FieldAngle.superClass_.dispose.call(this); - this.symbol_ = null; -}; - /** * Updates the graph when the field rerenders. * @private @@ -231,13 +222,10 @@ Blockly.FieldAngle.prototype.dropdownCreate_ = function() { }; /** - * Dispose of references to DOM elements and events belonging - * to the angle editor. + * Dispose of events belonging to the angle editor. * @private */ Blockly.FieldAngle.prototype.dropdownDispose_ = function() { - this.gauge_ = null; - this.line_ = null; Blockly.unbindEvent_(this.clickWrapper_); Blockly.unbindEvent_(this.moveWrapper1_); Blockly.unbindEvent_(this.moveWrapper2_); diff --git a/core/field_colour.js b/core/field_colour.js index 894f642f9..6fa02c696 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -332,8 +332,7 @@ Blockly.FieldColour.prototype.dropdownCreate_ = function() { }; /** - * Dispose of references to DOM elements and events belonging - * to the colour editor. + * Dispose of events belonging to the colour editor. * @private */ Blockly.FieldColour.prototype.dropdownDispose_ = function() { diff --git a/core/field_date.js b/core/field_date.js index 843d53562..83f6a1c34 100644 --- a/core/field_date.js +++ b/core/field_date.js @@ -240,7 +240,6 @@ Blockly.FieldDate.prototype.dropdownCreate_ = function() { * @private */ Blockly.FieldDate.prototype.dropdownDispose_ = function() { - this.picker_ = null; goog.events.unlistenByKey(this.changeEventKey_); goog.events.unlistenByKey(this.activeMonthEventKey_); }; diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 5b1272f2d..fd6863f7e 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -154,15 +154,6 @@ Blockly.FieldDropdown.prototype.initView = function() { } }; -/** - * Dispose of references to DOM elements and events belonging to this field. - */ -Blockly.FieldDropdown.prototype.dispose = function() { - Blockly.FieldDropdown.superClass_.dispose.call(this); - this.imageElement_ = null; - this.arrow_ = null; -}; - /** * Create a dropdown menu under the text. * @private @@ -223,8 +214,11 @@ Blockly.FieldDropdown.prototype.widgetCreate_ = function() { return menu; }; +/** + * Dispose of events belonging to the dropdown editor. + * @private + */ Blockly.FieldDropdown.prototype.widgetDispose_ = function() { - this.menu_ = null; goog.events.unlistenByKey(this.menuActionEventKey_); }; diff --git a/core/field_image.js b/core/field_image.js index 7a7bb96ec..bd5a56ae1 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -129,16 +129,6 @@ Blockly.FieldImage.prototype.initView = function() { 'xlink:href', this.value_); }; -/** - * Dispose of all DOM objects belonging to this text. - */ -Blockly.FieldImage.prototype.dispose = function() { - Blockly.FieldImage.superClass_.dispose.call(this); - this.imageElement_ = null; - // TODO: Do we need to dispose of this? - this.clickHandler_ = null; -}; - /** * Ensure that the input value (the source URL) is a string. * @param {string=} newValue The input value diff --git a/core/field_textinput.js b/core/field_textinput.js index 20636c8e8..089d62b8f 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -232,6 +232,11 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { } }; +/** + * Create the text input editor widget. + * @return {HTMLInputElement} The newly created text input editor. + * @private + */ Blockly.FieldTextInput.prototype.widgetCreate_ = function() { var div = Blockly.WidgetDiv.DIV; @@ -254,6 +259,42 @@ Blockly.FieldTextInput.prototype.widgetCreate_ = function() { return htmlInput; }; +/** + * Close the editor, save the results, and dispose any events bound to the + * text input's editor. + * @private + */ +Blockly.FieldTextInput.prototype.widgetDispose_ = function() { + // Finalize value. + this.isBeingEdited_ = false; + // No need to call setValue because if the widget is being closed the + // latest input text has already been validated. + if (this.value_ !== this.text_) { + // At the end of an edit the text should be the same as the value. It + // may not be if the input text is different than the validated text. + // We should fix that. + this.text_ = String(this.value_); + this.isTextValid_ = true; + this.forceRerender(); + } + // Otherwise don't rerender. + + // Call onFinishEditing + // TODO: Get rid of this or make it less of a hack. + if (this.onFinishEditing_) { + this.onFinishEditing_(this.value_); + } + + // Remove htmlInput events. + this.unbindInputEvents_(); + + // Delete style properties. + var style = Blockly.WidgetDiv.DIV.style; + style.width = 'auto'; + style.height = 'auto'; + style.fontSize = ''; +}; + /** * Bind handlers for user input on the text input field's editor. * @param {!HTMLInputElement} htmlInput The htmlInput to which event @@ -365,43 +406,6 @@ Blockly.FieldTextInput.prototype.resizeEditor_ = function() { div.style.top = xy.y + 'px'; }; -/** - * Close the editor, save the results, and dispose of the editable - * text field's elements. - * @private - */ -Blockly.FieldTextInput.prototype.widgetDispose_ = function() { - // Finalize value. - this.isBeingEdited_ = false; - // No need to call setValue because if the widget is being closed the - // latest input text has already been validated. - if (this.value_ !== this.text_) { - // At the end of an edit the text should be the same as the value. It - // may not be if the input text is different than the validated text. - // We should fix that. - this.text_ = String(this.value_); - this.isTextValid_ = true; - this.forceRerender(); - } - // Otherwise don't rerender. - - // Call onFinishEditing - // TODO: Get rid of this or make it less of a hack. - if (this.onFinishEditing_) { - this.onFinishEditing_(this.value_); - } - - // Remove htmlInput. - this.unbindInputEvents_(); - this.htmlInput_ = null; - - // Delete style properties. - var style = Blockly.WidgetDiv.DIV.style; - style.width = 'auto'; - style.height = 'auto'; - style.fontSize = ''; -}; - /** * Ensure that only a number may be entered. * @param {string} text The user's text. diff --git a/core/field_variable.js b/core/field_variable.js index 29aa44a0c..278fe5c55 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -157,16 +157,6 @@ Blockly.FieldVariable.prototype.toXml = function(fieldElement) { return fieldElement; }; -/** - * Dispose of this field. - * @public - */ -Blockly.FieldVariable.prototype.dispose = function() { - Blockly.FieldVariable.superClass_.dispose.call(this); - this.workspace_ = null; - this.variableMap_ = null; -}; - /** * Attach this field to a block. * @param {!Blockly.Block} block The block containing this field. From 81035c3bcb5ba5e696b7122027177ea232be0eac Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 7 Jun 2019 07:22:08 -0700 Subject: [PATCH 04/13] Fixed unit tests. --- core/field_textinput.js | 8 -------- tests/mocha/field_angle_test.js | 14 +++++++------- tests/mocha/field_number_test.js | 14 +++++++------- tests/mocha/field_textinput_test.js | 12 ++++++------ 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/core/field_textinput.js b/core/field_textinput.js index 089d62b8f..2e6c438e5 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -88,14 +88,6 @@ Blockly.FieldTextInput.prototype.SERIALIZABLE = true; */ Blockly.FieldTextInput.FONTSIZE = 11; -/** - * The HTML input element for the user to type, or null if no FieldTextInput - * editor is currently open. - * @type {HTMLInputElement} - * @protected - */ -Blockly.FieldTextInput.htmlInput_ = null; - /** * Mouse cursor style when over the hotspot that initiates the editor. */ diff --git a/tests/mocha/field_angle_test.js b/tests/mocha/field_angle_test.js index 7453c0a81..9a8252834 100644 --- a/tests/mocha/field_angle_test.js +++ b/tests/mocha/field_angle_test.js @@ -201,13 +201,13 @@ suite ('Angle Fields', function() { suite('Validators', function() { setup(function() { this.angleField = new Blockly.FieldAngle(1); - Blockly.FieldTextInput.htmlInput_ = Object.create(null); - Blockly.FieldTextInput.htmlInput_.oldValue_ = '1'; - Blockly.FieldTextInput.htmlInput_.untypedDefaultValue_ = 1; + this.angleField.htmlInput_ = Object.create(null); + this.angleField.htmlInput_.oldValue_ = '1'; + this.angleField.htmlInput_.untypedDefaultValue_ = 1; }); teardown(function() { this.angleField.setValidator(null); - Blockly.FieldTextInput.htmlInput_ = null; + this.angleField.htmlInput_ = null; }); suite('Null Validator', function() { setup(function() { @@ -217,7 +217,7 @@ suite ('Angle Fields', function() { }); test('When Editing', function() { this.angleField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = '2'; + this.angleField.htmlInput_.value = '2'; this.angleField.onHtmlInputChange_(null); assertValue(this.angleField, 1, '2'); this.angleField.isBeingEdited_ = false; @@ -235,7 +235,7 @@ suite ('Angle Fields', function() { }); test('When Editing', function() { this.angleField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = '25'; + this.angleField.htmlInput_.value = '25'; this.angleField.onHtmlInputChange_(null); assertValue(this.angleField, 30, '25'); this.angleField.isBeingEdited_ = false; @@ -251,7 +251,7 @@ suite ('Angle Fields', function() { }); test('When Editing', function() { this.angleField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = '2'; + this.angleField.htmlInput_.value = '2'; this.angleField.onHtmlInputChange_(null); assertValue(this.angleField, 2); this.angleField.isBeingEdited_ = false; diff --git a/tests/mocha/field_number_test.js b/tests/mocha/field_number_test.js index 5485c2d94..4915c8b7f 100644 --- a/tests/mocha/field_number_test.js +++ b/tests/mocha/field_number_test.js @@ -298,13 +298,13 @@ suite ('Number Fields', function() { suite('Validators', function() { setup(function() { this.numberField = new Blockly.FieldNumber(1); - Blockly.FieldTextInput.htmlInput_ = Object.create(null); - Blockly.FieldTextInput.htmlInput_.oldValue_ = '1'; - Blockly.FieldTextInput.htmlInput_.untypedDefaultValue_ = 1; + this.numberField.htmlInput_ = Object.create(null); + this.numberField.htmlInput_.oldValue_ = '1'; + this.numberField.htmlInput_.untypedDefaultValue_ = 1; }); teardown(function() { this.numberField.setValidator(null); - Blockly.FieldTextInput.htmlInput_ = null; + this.numberField.htmlInput_ = null; }); suite('Null Validator', function() { setup(function() { @@ -314,7 +314,7 @@ suite ('Number Fields', function() { }); test('When Editing', function() { this.numberField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = '2'; + this.numberField.htmlInput_.value = '2'; this.numberField.onHtmlInputChange_(null); assertValue(this.numberField, 1, '2'); this.numberField.isBeingEdited_ = false; @@ -332,7 +332,7 @@ suite ('Number Fields', function() { }); test('When Editing', function() { this.numberField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = '25'; + this.numberField.htmlInput_.value = '25'; this.numberField.onHtmlInputChange_(null); assertValue(this.numberField, 26, '25'); this.numberField.isBeingEdited_ = false; @@ -348,7 +348,7 @@ suite ('Number Fields', function() { }); test('When Editing', function() { this.numberField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = '2'; + this.numberField.htmlInput_.value = '2'; this.numberField.onHtmlInputChange_(null); assertValue(this.numberField, 2); this.numberField.isBeingEdited_ = false; diff --git a/tests/mocha/field_textinput_test.js b/tests/mocha/field_textinput_test.js index 30ed5b688..9b4e943be 100644 --- a/tests/mocha/field_textinput_test.js +++ b/tests/mocha/field_textinput_test.js @@ -169,9 +169,9 @@ suite ('Text Input Fields', function() { suite('Validators', function() { setup(function() { this.textInputField = new Blockly.FieldTextInput('value'); - Blockly.FieldTextInput.htmlInput_ = Object.create(null); - Blockly.FieldTextInput.htmlInput_.oldValue_ = 'value'; - Blockly.FieldTextInput.htmlInput_.untypedDefaultValue_ = 'value'; + this.textInputField.htmlInput_ = Object.create(null); + this.textInputField.htmlInput_.oldValue_ = 'value'; + this.textInputField.htmlInput_.untypedDefaultValue_ = 'value'; }); teardown(function() { this.textInputField.setValidator(null); @@ -185,7 +185,7 @@ suite ('Text Input Fields', function() { }); test('When Editing', function() { this.textInputField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = 'newValue'; + this.textInputField.htmlInput_.value = 'newValue'; this.textInputField.onHtmlInputChange_(null); assertValue(this.textInputField, 'value', 'newValue'); this.textInputField.isBeingEdited_ = false; @@ -203,7 +203,7 @@ suite ('Text Input Fields', function() { }); test('When Editing', function() { this.textInputField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = 'bbbaaa'; + this.textInputField.htmlInput_.value = 'bbbaaa'; this.textInputField.onHtmlInputChange_(null); assertValue(this.textInputField, 'bbb', 'bbbaaa'); this.textInputField.isBeingEdited_ = false; @@ -219,7 +219,7 @@ suite ('Text Input Fields', function() { }); test('When Editing', function() { this.textInputField.isBeingEdited_ = true; - Blockly.FieldTextInput.htmlInput_.value = 'newValue'; + this.textInputField.htmlInput_.value = 'newValue'; this.textInputField.onHtmlInputChange_(null); assertValue(this.textInputField, 'newValue'); this.textInputField.isBeingEdited_ = false; From 153817eaee1506ad83d557332a6399ca8fc26670 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Mon, 10 Jun 2019 15:26:44 -0700 Subject: [PATCH 05/13] Removed angle field WidgetDiv.hide call. --- core/field_angle.js | 1 - 1 file changed, 1 deletion(-) diff --git a/core/field_angle.js b/core/field_angle.js index 13e264216..a4578c3ea 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -237,7 +237,6 @@ Blockly.FieldAngle.prototype.dropdownDispose_ = function() { */ Blockly.FieldAngle.prototype.hide_ = function() { Blockly.DropDownDiv.hideIfOwner(this); - Blockly.WidgetDiv.hide(); }; /** From 7af5c5147ac349c4f8596a246a9726d509192aa8 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Mon, 10 Jun 2019 15:38:34 -0700 Subject: [PATCH 06/13] Fixed dropdown div not fading in correctly. --- core/dropdowndiv.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index b624b3448..c7cb54ba3 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -139,6 +139,8 @@ Blockly.DropDownDiv.createDom = function() { div.appendChild(arrow); Blockly.DropDownDiv.arrow_ = arrow; + Blockly.DropDownDiv.DIV_.style.opacity = 0; + // Transition animation for transform: translate() and opacity. Blockly.DropDownDiv.DIV_.style.transition = 'transform ' + Blockly.DropDownDiv.ANIMATION_TIME + 's, ' + From 8ee266ea65b15057fdfd686b7c49ea8189d180a7 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Mon, 10 Jun 2019 16:14:17 -0700 Subject: [PATCH 07/13] Fixed dropdowndiv not animating correctly after hideChaff. Extracted dual functionality of positionInternal_ and moved it to hideWithoutAnimation. --- core/dropdowndiv.js | 60 +++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index c7cb54ba3..71ffe7368 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -463,7 +463,16 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { if (Blockly.DropDownDiv.animateOutTimer_) { clearTimeout(Blockly.DropDownDiv.animateOutTimer_); } - Blockly.DropDownDiv.positionInternal_(); + + // Reset style properties in case this gets called directly + // instead of hide() - see discussion on #2551. + var div = Blockly.DropDownDiv.DIV_; + div.style.transform = ''; + div.style.left = ''; + div.style.top = ''; + div.style.opacity = 0; + div.style.display = 'none'; + Blockly.DropDownDiv.clearContent(); Blockly.DropDownDiv.owner_ = null; if (Blockly.DropDownDiv.onHide_) { @@ -475,37 +484,36 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { /** * Set the dropdown div's position. - * @param {number} initialX Initial Horizontal location (window coordinates, not body). - * @param {number} initialY Initial Vertical location (window coordinates, not body). - * @param {number} finalX Final Horizontal location (window coordinates, not body). - * @param {number} finalY Final Vertical location (window coordinates, not body). + * @param {!number} initialX Initial Horizontal location + * (window coordinates, not body). + * @param {!number} initialY Initial Vertical location + * (window coordinates, not body). + * @param {!number} finalX Final Horizontal location + * (window coordinates, not body). + * @param {!number} finalY Final Vertical location + * (window coordinates, not body). * @private */ Blockly.DropDownDiv.positionInternal_ = function(initialX, initialY, finalX, finalY) { - initialX = initialX == null ? initialX : Math.floor(initialX); - initialY = initialY == null ? initialY : Math.floor(initialY); - finalX = finalX == null ? finalX : Math.floor(finalX); - finalY = finalY == null ? finalY : Math.floor(finalY); + initialX = Math.floor(initialX); + initialY = Math.floor(initialY); + finalX = Math.floor(finalX); + finalY = Math.floor(finalY); var div = Blockly.DropDownDiv.DIV_; // First apply initial translation. - div.style.left = initialX != null ? initialX + 'px' : ''; - div.style.top = initialY != null ? initialY + 'px' : ''; - if (finalX != null) { - // Show the div. - div.style.display = 'block'; - div.style.opacity = 1; - // Add final translate, animated through `transition`. - // Coordinates are relative to (initialX, initialY), - // where the drop-down is absolutely positioned. - var dx = (finalX - initialX); - var dy = (finalY - initialY); - div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)'; - } else { - // Hide the div. - div.style.display = 'none'; - div.style.transform = ''; - } + div.style.left = initialX + 'px'; + div.style.top = initialY + 'px'; + + // Show the div. + div.style.display = 'block'; + div.style.opacity = 1; + // Add final translate, animated through `transition`. + // Coordinates are relative to (initialX, initialY), + // where the drop-down is absolutely positioned. + var dx = (finalX - initialX); + var dy = (finalY - initialY); + div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)'; }; /** From 6d47853370f5f656f9b34a01277e88dae187b972 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 11 Jun 2019 07:03:03 -0700 Subject: [PATCH 08/13] Fixed workspace scroll not properly hiding floating elements. --- core/workspace_svg.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 7cedea7ee..52f7dae05 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -1913,6 +1913,8 @@ Blockly.WorkspaceSvg.prototype.setScale = function(newScale) { * @package */ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { + Blockly.hideChaff(/* opt_allowToolbox */ true); + // Keep scrolling within the bounds of the content. var metrics = this.getMetrics(); // This is the offset of the top-left corner of the view from the @@ -1945,10 +1947,6 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) { this.scrollbar.vScroll.setHandlePosition(-(y + metrics.contentTop) * this.scrollbar.vScroll.ratio_); } - - // Hide the WidgetDiv without animation. This is to prevent a disposal - // animation from happening in the wrong location. - Blockly.WidgetDiv.hide(true); // We have to shift the translation so that when the canvas is at 0, 0 the // workspace origin is not underneath the toolbox. x += metrics.absoluteLeft; From 2af17fd782d9f2e22337bd57c52dab0a5e5b030d Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 11 Jun 2019 07:07:14 -0700 Subject: [PATCH 09/13] Removed obsolete widgetDiv check in tooltip code. --- core/tooltip.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/tooltip.js b/core/tooltip.js index eded96dd4..24d661cc0 100644 --- a/core/tooltip.js +++ b/core/tooltip.js @@ -211,9 +211,6 @@ Blockly.Tooltip.onMouseMove_ = function(e) { if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) { // No tooltip here to show. return; - } else if (Blockly.WidgetDiv.isVisible()) { - // Don't display a tooltip if a widget is open (tooltip would be under it). - return; } else if (Blockly.Tooltip.blocked_) { // Someone doesn't want us to show tooltips. We are probably handling a // user gesture, such as a click or drag. From 392119680e46b355eb15243c7ca17f18610a783f Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 11 Jun 2019 07:12:57 -0700 Subject: [PATCH 10/13] Fixed misc PR comments. --- core/dropdowndiv.js | 12 ++++++------ core/field_angle.js | 3 +-- core/field_textinput.js | 11 +++-------- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index 71ffe7368..62e310794 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -484,13 +484,13 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { /** * Set the dropdown div's position. - * @param {!number} initialX Initial Horizontal location + * @param {number} initialX Initial Horizontal location * (window coordinates, not body). - * @param {!number} initialY Initial Vertical location + * @param {number} initialY Initial Vertical location * (window coordinates, not body). - * @param {!number} finalX Final Horizontal location + * @param {number} finalX Final Horizontal location * (window coordinates, not body). - * @param {!number} finalY Final Vertical location + * @param {number} finalY Final Vertical location * (window coordinates, not body). * @private */ @@ -511,8 +511,8 @@ Blockly.DropDownDiv.positionInternal_ = function(initialX, initialY, finalX, fin // Add final translate, animated through `transition`. // Coordinates are relative to (initialX, initialY), // where the drop-down is absolutely positioned. - var dx = (finalX - initialX); - var dy = (finalY - initialY); + var dx = finalX - initialX; + var dy = finalY - initialY; div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)'; }; diff --git a/core/field_angle.js b/core/field_angle.js index a4578c3ea..b6bdfebbb 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -155,8 +155,7 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { Blockly.DropDownDiv.getContentDiv().appendChild(editor); var border = this.sourceBlock_.getColourBorder(); - border = border.colourBorder == null ? - border.colourLight : border.colourBorder; + border = border.colourBorder || border.colourLight; Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), border); Blockly.DropDownDiv.showPositionedByField( diff --git a/core/field_textinput.js b/core/field_textinput.js index 2e6c438e5..df82ee4d6 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -26,7 +26,6 @@ goog.provide('Blockly.FieldTextInput'); -goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockChange'); goog.require('Blockly.Field'); @@ -226,7 +225,7 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { /** * Create the text input editor widget. - * @return {HTMLInputElement} The newly created text input editor. + * @return {!HTMLInputElement} The newly created text input editor. * @private */ Blockly.FieldTextInput.prototype.widgetCreate_ = function() { @@ -236,7 +235,7 @@ Blockly.FieldTextInput.prototype.widgetCreate_ = function() { htmlInput.className = 'blocklyHtmlInput'; htmlInput.setAttribute('spellcheck', this.spellcheck_); var fontSize = - (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; + (Blockly.FieldTextInput.FONTSIZE * this.workspace_.scale) + 'pt'; div.style.fontSize = fontSize; htmlInput.style.fontSize = fontSize; div.appendChild(htmlInput); @@ -320,8 +319,7 @@ Blockly.FieldTextInput.prototype.unbindInputEvents_ = function() { Blockly.unbindEvent_(this.onKeyDownWrapper_); Blockly.unbindEvent_(this.onKeyUpWrapper_); Blockly.unbindEvent_(this.onKeyPressWrapper_); - this.workspace_.removeChangeListener( - this.onWorkspaceChangeWrapper_); + this.workspace_.removeChangeListener(this.onWorkspaceChangeWrapper_); }; /** @@ -333,14 +331,11 @@ Blockly.FieldTextInput.prototype.onHtmlInputKeyDown_ = function(e) { var tabKey = 9, enterKey = 13, escKey = 27; if (e.keyCode == enterKey) { Blockly.WidgetDiv.hide(); - Blockly.DropDownDiv.hideIfOwner(this); } else if (e.keyCode == escKey) { this.htmlInput_.value = this.htmlInput_.defaultValue; Blockly.WidgetDiv.hide(); - Blockly.DropDownDiv.hideIfOwner(this); } else if (e.keyCode == tabKey) { Blockly.WidgetDiv.hide(); - Blockly.DropDownDiv.hideIfOwner(this); this.sourceBlock_.tab(this, !e.shiftKey); e.preventDefault(); } From 4b0397de1731717171fd8744735a4d3fef635029 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 11 Jun 2019 13:48:01 -0700 Subject: [PATCH 11/13] Cleaned up utils renamings. Readded widget div hide in field angle. --- core/field_angle.js | 15 ++++++++------- core/field_dropdown.js | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/core/field_angle.js b/core/field_angle.js index b6bdfebbb..465909a6b 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -146,9 +146,9 @@ Blockly.FieldAngle.prototype.render_ = function() { Blockly.FieldAngle.prototype.showEditor_ = function() { // Mobile browsers have issues with in-line textareas (focus & keyboards). var noFocus = - Blockly.userAgent.MOBILE || - Blockly.userAgent.ANDROID || - Blockly.userAgent.IPAD; + Blockly.utils.userAgent.MOBILE || + Blockly.utils.userAgent.ANDROID || + Blockly.utils.userAgent.IPAD; Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); var editor = this.dropdownCreate_(); @@ -170,7 +170,7 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { * @private */ Blockly.FieldAngle.prototype.dropdownCreate_ = function() { - var svg = Blockly.utils.createSvgElement('svg', { + var svg = Blockly.utils.dom.createSvgElement('svg', { 'xmlns': 'http://www.w3.org/2000/svg', 'xmlns:html': 'http://www.w3.org/1999/xhtml', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', @@ -178,16 +178,16 @@ Blockly.FieldAngle.prototype.dropdownCreate_ = function() { 'height': (Blockly.FieldAngle.HALF * 2) + 'px', 'width': (Blockly.FieldAngle.HALF * 2) + 'px' }, null); - var circle = Blockly.utils.createSvgElement('circle', { + var circle = Blockly.utils.dom.createSvgElement('circle', { 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, 'r': Blockly.FieldAngle.RADIUS, 'class': 'blocklyAngleCircle' }, svg); - this.gauge_ = Blockly.utils.createSvgElement('path', { + this.gauge_ = Blockly.utils.dom.createSvgElement('path', { 'class': 'blocklyAngleGauge' }, svg); - this.line_ = Blockly.utils.createSvgElement('line', { + this.line_ = Blockly.utils.dom.createSvgElement('line', { 'x1': Blockly.FieldAngle.HALF, 'y1': Blockly.FieldAngle.HALF, 'class': 'blocklyAngleLine' @@ -236,6 +236,7 @@ Blockly.FieldAngle.prototype.dropdownDispose_ = function() { */ Blockly.FieldAngle.prototype.hide_ = function() { Blockly.DropDownDiv.hideIfOwner(this); + Blockly.WidgetDiv.hide(); }; /** diff --git a/core/field_dropdown.js b/core/field_dropdown.js index fd6863f7e..6f1360224 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -165,7 +165,7 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() { this.menu_.render(Blockly.WidgetDiv.DIV); // Element gets created in render. - Blockly.utils.addClass(this.menu_.getElement(), 'blocklyDropdownMenu'); + Blockly.utils.dom.addClass(this.menu_.getElement(), 'blocklyDropdownMenu'); this.positionMenu_(this.menu_); From 6f69b856395552683a8f5ce535da29c1fbd60937 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 11 Jun 2019 14:24:48 -0700 Subject: [PATCH 12/13] Removed unnecessary todo's. --- core/field_date.js | 1 - core/field_dropdown.js | 1 - 2 files changed, 2 deletions(-) diff --git a/core/field_date.js b/core/field_date.js index 83f6a1c34..f507dca25 100644 --- a/core/field_date.js +++ b/core/field_date.js @@ -246,7 +246,6 @@ Blockly.FieldDate.prototype.dropdownDispose_ = function() { /** * Handle a CHANGE event in the date picker. - * TODO: Not sure what the type for goog event information is. * @param {!Event} event The CHANGE event. * @private */ diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 6f1360224..b2bf6bb72 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -224,7 +224,6 @@ Blockly.FieldDropdown.prototype.widgetDispose_ = function() { /** * Handle an ACTION event in the dropdown menu. - * TODO: Not sure what the type for goog event information is. * @param {!Event} event The CHANGE event. * @private */ From efdfec6d8a12a0bc83edd608b5f9063f247deb27 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 11 Jun 2019 14:29:19 -0700 Subject: [PATCH 13/13] Fixed last rebase error. --- core/field_angle.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/field_angle.js b/core/field_angle.js index 465909a6b..75e65fb62 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -171,9 +171,9 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { */ Blockly.FieldAngle.prototype.dropdownCreate_ = function() { var svg = Blockly.utils.dom.createSvgElement('svg', { - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:html': 'http://www.w3.org/1999/xhtml', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + 'xmlns': Blockly.utils.dom.SVG_NS, + 'xmlns:html': Blockly.utils.dom.HTML_NS, + 'xmlns:xlink': Blockly.utils.dom.XLINK_NS, 'version': '1.1', 'height': (Blockly.FieldAngle.HALF * 2) + 'px', 'width': (Blockly.FieldAngle.HALF * 2) + 'px'