diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index b624b3448..62e310794 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, ' + @@ -461,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_) { @@ -473,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)'; }; /** diff --git a/core/field.js b/core/field.js index a5302527f..ecd8469cb 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. @@ -328,21 +327,20 @@ 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() { + Blockly.DropDownDiv.hideIfOwner(this); + Blockly.WidgetDiv.hideIfOwner(this); + 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; }; /** @@ -785,12 +783,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..75e65fb62 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -140,42 +140,36 @@ 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() { + // Mobile browsers have issues with in-line textareas (focus & keyboards). var noFocus = Blockly.utils.userAgent.MOBILE || Blockly.utils.userAgent.ANDROID || Blockly.utils.userAgent.IPAD; - // Mobile browsers have issues with in-line textareas (focus & keyboards). 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 border = this.sourceBlock_.getColourBorder(); + border = border.colourBorder || border.colourLight; + 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.dom.createSvgElement('svg', { 'xmlns': Blockly.utils.dom.SVG_NS, 'xmlns:html': Blockly.utils.dom.HTML_NS, @@ -183,14 +177,16 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { 'version': '1.1', 'height': (Blockly.FieldAngle.HALF * 2) + 'px', 'width': (Blockly.FieldAngle.HALF * 2) + 'px' - }, div); + }, null); var circle = Blockly.utils.dom.createSvgElement('circle', { - 'cx': Blockly.FieldAngle.HALF, 'cy': Blockly.FieldAngle.HALF, + '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.gauge_ = Blockly.utils.dom.createSvgElement('path', { + 'class': 'blocklyAngleGauge' + }, svg); this.line_ = Blockly.utils.dom.createSvgElement('line', { 'x1': Blockly.FieldAngle.HALF, 'y1': Blockly.FieldAngle.HALF, @@ -210,33 +206,35 @@ 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 events belonging to the angle editor. + * @private + */ +Blockly.FieldAngle.prototype.dropdownDispose_ = function() { + 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 +285,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..6fa02c696 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,19 @@ 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 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..f507dca25 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,41 @@ 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_); - } - this.picker_ = null; - Blockly.Events.setGroup(false); +Blockly.FieldDate.prototype.dropdownDispose_ = function() { + goog.events.unlistenByKey(this.changeEventKey_); + goog.events.unlistenByKey(this.activeMonthEventKey_); +}; + +/** + * Handle a CHANGE event in the date picker. + * @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..b2bf6bb72 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -159,42 +159,32 @@ Blockly.FieldDropdown.prototype.initView = function() { * @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.dom.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 +203,35 @@ 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; }; +/** + * Dispose of events belonging to the dropdown editor. + * @private + */ +Blockly.FieldDropdown.prototype.widgetDispose_ = function() { + goog.events.unlistenByKey(this.menuActionEventKey_); +}; + +/** + * Handle an ACTION event in the dropdown menu. + * @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 +240,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 +254,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 +504,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..bd5a56ae1 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -129,17 +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() { - if (this.fieldGroup_) { - Blockly.utils.dom.removeNode(this.fieldGroup_); - this.fieldGroup_ = null; - } - this.imageElement_ = null; -}; - /** * Ensure that the input value (the source URL) is a string. * @param {string=} newValue The input value 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..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'); @@ -88,14 +87,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. */ @@ -107,14 +98,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 +120,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 +157,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,14 +212,25 @@ 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(); + } +}; + +/** + * 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; - // Create the input. var htmlInput = document.createElement('input'); htmlInput.className = 'blocklyHtmlInput'; htmlInput.setAttribute('spellcheck', this.spellcheck_); @@ -250,59 +240,86 @@ Blockly.FieldTextInput.prototype.showInlineEditor_ = function(quietInput) { 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. + * 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 + * handlers will be bound. * @private */ Blockly.FieldTextInput.prototype.bindInputEvents_ = function(htmlInput) { - // Bind to keydown -- trap Enter without IME and Esc to hide. - htmlInput.onKeyDownWrapper_ = + // 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_ = + // 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_ = + // 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_); + + // 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_); - this.workspace_.removeChangeListener( - htmlInput.onWorkspaceChangeWrapper_); +Blockly.FieldTextInput.prototype.unbindInputEvents_ = function() { + Blockly.unbindEvent_(this.onKeyDownWrapper_); + Blockly.unbindEvent_(this.onKeyUpWrapper_); + Blockly.unbindEvent_(this.onKeyPressWrapper_); + this.workspace_.removeChangeListener(this.onWorkspaceChangeWrapper_); }; /** @@ -311,18 +328,14 @@ 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) { Blockly.WidgetDiv.hide(); - Blockly.DropDownDiv.hideIfOwner(this); this.sourceBlock_.tab(this, !e.shiftKey); e.preventDefault(); } @@ -334,10 +347,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 +357,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); } @@ -381,49 +393,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. - * @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. - 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 (thisField.onFinishEditing_) { - thisField.onFinishEditing_(thisField.value_); - } - - // 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 = ''; - }; -}; - /** * 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. 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. 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; 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;