From 91408a3238f7dedb16a376e78086bb4a0b055ca7 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 8 Feb 2019 12:45:24 -0800 Subject: [PATCH 1/9] Add dropdowndiv file --- core/css.js | 42 ++++ core/dropdowndiv.js | 516 ++++++++++++++++++++++++++++++++++++++++++++ core/inject.js | 2 + 3 files changed, 560 insertions(+) create mode 100644 core/dropdowndiv.js diff --git a/core/css.js b/core/css.js index 66a789e9b..db0133b03 100644 --- a/core/css.js +++ b/core/css.js @@ -183,6 +183,48 @@ Blockly.Css.CONTENT = [ 'z-index: 100000;', /* big value for bootstrap3 compatibility */ '}', + '.blocklyDropDownDiv {', + 'position: fixed;', + 'left: 0;', + 'top: 0;', + 'z-index: 1000;', + 'display: none;', + 'border: 1px solid;', + 'border-radius: 4px;', + 'box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3);', + 'padding: 4px;', + '-webkit-user-select: none;', + '}', + + '.blocklyDropDownContent {', + 'max-height: 300px;', // @todo: spec for maximum height. + 'overflow: auto;', + 'overflow-x: hidden;', + '}', + + '.blocklyDropDownArrow {', + 'position: absolute;', + 'left: 0;', + 'top: 0;', + 'width: 16px;', + 'height: 16px;', + 'z-index: -1;', + 'background-color: inherit;', + 'border-color: inherit;', + '}', + + '.blocklyDropDownButton {', + 'display: inline-block;', + 'float: left;', + 'padding: 0;', + 'margin: 4px;', + 'border-radius: 4px;', + 'outline: none;', + 'border: 1px solid;', + 'transition: box-shadow .1s;', + 'cursor: pointer;', + '}', + '.blocklyResizeSE {', 'cursor: se-resize;', 'fill: #aaa;', diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js new file mode 100644 index 000000000..4445eb545 --- /dev/null +++ b/core/dropdowndiv.js @@ -0,0 +1,516 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2016 Massachusetts Institute of Technology + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview A div that floats on top of the workspace, for drop-down menus. + * The drop-down can be kept inside the workspace, animate in/out, etc. + * @author tmickel@mit.edu (Tim Mickel) + */ + +'use strict'; + +goog.provide('Blockly.DropDownDiv'); + +goog.require('goog.dom'); +goog.require('goog.style'); + +/** + * Class for drop-down div. + * @constructor + */ +Blockly.DropDownDiv = function() { +}; + +/** + * The div element. Set once by Blockly.DropDownDiv.createDom. + * @type {Element} + * @private + */ +Blockly.DropDownDiv.DIV_ = null; + +/** + * Drop-downs will appear within the bounds of this element if possible. + * Set in Blockly.DropDownDiv.setBoundsElement. + * @type {Element} + * @private + */ +Blockly.DropDownDiv.boundsElement_ = null; + +/** + * The object currently using the drop-down. + * @type {Object} + * @private + */ +Blockly.DropDownDiv.owner_ = null; + +/** + * Whether the dropdown was positioned to a field or the source block. + * @type {boolean} + * @private + */ +Blockly.DropDownDiv.positionToField_ = null; + +/** + * Arrow size in px. Should match the value in CSS (need to position pre-render). + * @type {number} + * @const + */ +Blockly.DropDownDiv.ARROW_SIZE = 16; + +/** + * Drop-down border size in px. Should match the value in CSS (need to position the arrow). + * @type {number} + * @const + */ +Blockly.DropDownDiv.BORDER_SIZE = 1; + +/** + * Amount the arrow must be kept away from the edges of the main drop-down div, in px. + * @type {number} + * @const + */ +Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING = 12; + +/** + * Amount drop-downs should be padded away from the source, in px. + * @type {number} + * @const + */ +Blockly.DropDownDiv.PADDING_Y = 20; + +/** + * Length of animations in seconds. + * @type {number} + * @const + */ +Blockly.DropDownDiv.ANIMATION_TIME = 0.25; + +/** + * Timer for animation out, to be cleared if we need to immediately hide + * without disrupting new shows. + * @type {number} + */ +Blockly.DropDownDiv.animateOutTimer_ = null; + +/** + * Callback for when the drop-down is hidden. + * @type {Function} + */ +Blockly.DropDownDiv.onHide_ = 0; + +/** + * Create and insert the DOM element for this div. + * @param {Element} container Element that the div should be contained in. + */ +Blockly.DropDownDiv.createDom = function() { + if (Blockly.DropDownDiv.DIV_) { + return; // Already created. + } + var div = document.createElement('div'); + div.className = 'blocklyDropDownDiv'; + document.body.appendChild(div); + Blockly.DropDownDiv.DIV_ = div; + + var content = document.createElement('div'); + content.className = 'blocklyDropDownContent'; + div.appendChild(content); + Blockly.DropDownDiv.content_ = content; + + var arrow = document.createElement('div'); + arrow.className = 'blocklyDropDownArrow'; + div.appendChild(arrow); + Blockly.DropDownDiv.arrow_ = arrow; + + // Transition animation for transform: translate() and opacity. + Blockly.DropDownDiv.DIV_.style.transition = 'transform ' + + Blockly.DropDownDiv.ANIMATION_TIME + 's, ' + + 'opacity ' + Blockly.DropDownDiv.ANIMATION_TIME + 's'; +}; + +/** + * Set an element to maintain bounds within. Drop-downs will appear + * within the box of this element if possible. + * @param {Element} boundsElement Element to bound drop-down to. + */ +Blockly.DropDownDiv.setBoundsElement = function(boundsElement) { + Blockly.DropDownDiv.boundsElement_ = boundsElement; +}; + +/** + * Provide the div for inserting content into the drop-down. + * @return {Element} Div to populate with content + */ +Blockly.DropDownDiv.getContentDiv = function() { + return Blockly.DropDownDiv.content_; +}; + +/** + * Clear the content of the drop-down. + */ +Blockly.DropDownDiv.clearContent = function() { + Blockly.DropDownDiv.content_.innerHTML = ''; + Blockly.DropDownDiv.content_.style.width = ''; +}; + +/** + * Set the colour for the drop-down. + * @param {string} backgroundColour Any CSS color for the background + * @param {string} borderColour Any CSS color for the border + */ +Blockly.DropDownDiv.setColour = function(backgroundColour, borderColour) { + Blockly.DropDownDiv.DIV_.style.backgroundColor = backgroundColour; + Blockly.DropDownDiv.DIV_.style.borderColor = borderColour; +}; + +/** + * Set the category for the drop-down. + * @param {string} category The new category for the drop-down. + */ +Blockly.DropDownDiv.setCategory = function(category) { + Blockly.DropDownDiv.DIV_.setAttribute('data-category', category); +}; + +/** + * Shortcut to show and place the drop-down with positioning determined + * by a particular block. The primary position will be below the block, + * and the secondary position above the block. Drop-down will be + * constrained to the block's workspace. + * @param {!Blockly.Field} field The field showing the drop-down + * @param {!Blockly.Block} block Block to position the drop-down around. + * @param {Function=} opt_onHide Optional callback for when the drop-down is hidden. + * @param {Number} opt_secondaryYOffset Optional Y offset for above-block positioning. + * @return {boolean} True if the menu rendered below block; false if above. + */ +Blockly.DropDownDiv.showPositionedByBlock = function(field, block, + opt_onHide, opt_secondaryYOffset) { + var scale = block.workspace.scale; + var bBox = {width: block.width, height: block.height}; + bBox.width *= scale; + bBox.height *= scale; + var position = block.getSvgRoot().getBoundingClientRect(); + // If we can fit it, render below the block. + var primaryX = position.left + bBox.width / 2; + var primaryY = position.top + bBox.height; + // If we can't fit it, render above the entire parent block. + var secondaryX = primaryX; + var secondaryY = position.top; + if (opt_secondaryYOffset) { + secondaryY += opt_secondaryYOffset; + } + // Set bounds to workspace; show the drop-down. + Blockly.DropDownDiv.setBoundsElement(block.workspace.getParentSvg().parentNode); + return Blockly.DropDownDiv.show(field, primaryX, primaryY, secondaryX, secondaryY, opt_onHide); +}; + + +/** + * Shortcut to show and place the drop-down with positioning determined + * by a particular field. The primary position will be below the field, + * and the secondary position above the field. Drop-down will be + * constrained to the block's workspace. + * @param {Object} owner The object showing the drop-down + * @param {Function=} opt_onHide Optional callback for when the drop-down is hidden. + * @param {Number} opt_secondaryYOffset Optional Y offset for above-block positioning. + * @return {boolean} True if the menu rendered below block; false if above. + */ +Blockly.DropDownDiv.showPositionedByField = function(owner, + opt_onHide, opt_secondaryYOffset) { + var scale = owner.sourceBlock_.workspace.scale; + var bBox = {width: owner.size_.width, height: owner.size_.height}; + bBox.width *= scale; + bBox.height *= scale; + var position = owner.fieldGroup_.getBoundingClientRect(); + // If we can fit it, render below the block. + var primaryX = position.left + bBox.width / 2; + var primaryY = position.top + bBox.height; + // If we can't fit it, render above the entire parent block. + var secondaryX = primaryX; + var secondaryY = position.top; + if (opt_secondaryYOffset) { + secondaryY += opt_secondaryYOffset; + } + // Set bounds to workspace; show the drop-down. + Blockly.DropDownDiv.positionToField_ = true; + Blockly.DropDownDiv.setBoundsElement(owner.sourceBlock_.workspace.getParentSvg().parentNode); + return Blockly.DropDownDiv.show(owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide); +}; + + +/** + * Show and place the drop-down. + * The drop-down is placed with an absolute "origin point" (x, y) - i.e., + * the arrow will point at this origin and box will positioned below or above it. + * If we can maintain the container bounds at the primary point, the arrow will + * point there, and the container will be positioned below it. + * If we can't maintain the container bounds at the primary point, fall-back to the + * secondary point and position above. + * @param {Object} owner The object showing the drop-down + * @param {number} primaryX Desired origin point x, in absolute px + * @param {number} primaryY Desired origin point y, in absolute px + * @param {number} secondaryX Secondary/alternative origin point x, in absolute px + * @param {number} secondaryY Secondary/alternative origin point y, in absolute px + * @param {Function=} opt_onHide Optional callback for when the drop-down is hidden + * @return {boolean} True if the menu rendered at the primary origin point. + */ +Blockly.DropDownDiv.show = function(owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) { + Blockly.DropDownDiv.owner_ = owner; + Blockly.DropDownDiv.onHide_ = opt_onHide; + var metrics = Blockly.DropDownDiv.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY); + // Update arrow CSS + Blockly.DropDownDiv.arrow_.style.transform = 'translate(' + + metrics.arrowX + 'px,' + metrics.arrowY + 'px) rotate(45deg)'; + Blockly.DropDownDiv.arrow_.setAttribute('class', + metrics.arrowAtTop ? 'blocklyDropDownArrow arrowTop' : 'blocklyDropDownArrow arrowBottom'); + Blockly.DropDownDiv.arrow_.style.display = + metrics.arrowVisible ? '' : 'none'; + + // When we change `translate` multiple times in close succession, + // Chrome may choose to wait and apply them all at once. + // Since we want the translation to initial X, Y to be immediate, + // and the translation to final X, Y to be animated, + // we saw problems where both would be applied after animation was turned on, + // making the dropdown appear to fly in from (0, 0). + // Using both `left`, `top` for the initial translation and then `translate` + // for the animated transition to final X, Y is a workaround. + + Blockly.DropDownDiv.positionInternal_( + metrics.initialX, metrics.initialY, + metrics.finalX, metrics.finalY); + return metrics.arrowAtTop; +}; + +/** + * Helper to position the drop-down and the arrow, maintaining bounds. + * See explanation of origin points in Blockly.DropDownDiv.show. + * @param {number} primaryX Desired origin point x, in absolute px + * @param {number} primaryY Desired origin point y, in absolute px + * @param {number} secondaryX Secondary/alternative origin point x, in absolute px + * @param {number} secondaryY Secondary/alternative origin point y, in absolute px + * @returns {Object} Various final metrics, including rendered positions for drop-down and arrow. + */ +Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX, secondaryY) { + var div = Blockly.DropDownDiv.DIV_; + var boundPosition = Blockly.DropDownDiv.boundsElement_.getBoundingClientRect(); + + var boundSize = goog.style.getSize(Blockly.DropDownDiv.boundsElement_); + var divSize = goog.style.getSize(div); + + // First decide if we will render at primary or secondary position + // i.e., above or below + // renderX, renderY will eventually be the final rendered position of the box. + var renderX, renderY, renderedSecondary, renderedTertiary; + // Can the div fit inside the bounds if we render below the primary point? + if (primaryY + divSize.height > boundPosition.top + boundSize.height) { + // We can't fit below in terms of y. Can we fit above? + if (secondaryY - divSize.height < boundPosition.top) { + // We also can't fit above, so just render at the top of the screen. + renderX = primaryX; + renderY = 0; + renderedSecondary = false; + renderedTertiary = true; + } else { + // We can fit above, render secondary + renderX = secondaryX; + renderY = secondaryY - divSize.height - Blockly.DropDownDiv.PADDING_Y; + renderedSecondary = true; + } + } else { + // We can fit below, render primary + renderX = primaryX; + renderY = primaryY + Blockly.DropDownDiv.PADDING_Y; + renderedSecondary = false; + } + // First calculate the absolute arrow X + // This needs to be done before positioning the div, since the arrow + // wants to be as close to the origin point as possible. + var arrowX = renderX - Blockly.DropDownDiv.ARROW_SIZE / 2; + // Keep in overall bounds + arrowX = Math.max(boundPosition.left, Math.min(arrowX, boundPosition.left + boundSize.width)); + + // Adjust the x-position of the drop-down so that the div is centered and within bounds. + var centerX = divSize.width / 2; + renderX -= centerX; + // Fit horizontally in the bounds. + renderX = Math.max( + boundPosition.left, + Math.min(renderX, boundPosition.left + boundSize.width - divSize.width) + ); + // After we've finished caclulating renderX, adjust the arrow to be relative to it. + arrowX -= renderX; + + // Pad the arrow by some pixels, primarily so that it doesn't render on top of a rounded border. + arrowX = Math.max( + Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING, + Math.min(arrowX, divSize.width - + Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING - Blockly.DropDownDiv.ARROW_SIZE) + ); + + // Calculate arrow Y. If we rendered secondary, add on bottom. + // Extra pixels are added so that it covers the border of the div. + var arrowY = (renderedSecondary) ? divSize.height - Blockly.DropDownDiv.BORDER_SIZE : 0; + arrowY -= (Blockly.DropDownDiv.ARROW_SIZE / 2) + Blockly.DropDownDiv.BORDER_SIZE; + + // Initial position calculated without any padding to provide an animation point. + var initialX = renderX; // X position remains constant during animation. + var initialY; + if (renderedSecondary) { + initialY = secondaryY - divSize.height; // No padding on Y + } else { + initialY = primaryY; // No padding on Y + } + + return { + initialX: initialX, + initialY : initialY, + finalX: renderX, + finalY: renderY, + arrowX: arrowX, + arrowY: arrowY, + arrowAtTop: !renderedSecondary, + arrowVisible: !renderedTertiary + }; +}; + +/** + * Is the container visible? + * @return {boolean} True if visible. + */ +Blockly.DropDownDiv.isVisible = function() { + return !!Blockly.DropDownDiv.owner_; +}; + +/** + * Hide the menu only if it is owned by the provided object. + * @param {Object} owner Object which must be owning the drop-down to hide + * @return {Boolean} True if hidden + */ +Blockly.DropDownDiv.hideIfOwner = function(owner) { + if (Blockly.DropDownDiv.owner_ === owner) { + Blockly.DropDownDiv.hide(); + return true; + } + return false; +}; + +/** + * Hide the menu, triggering animation. + */ +Blockly.DropDownDiv.hide = function() { + // Start the animation by setting the translation and fading out. + var div = Blockly.DropDownDiv.DIV_; + // Reset to (initialX, initialY) - i.e., no translation. + div.style.transform = 'translate(0px, 0px)'; + div.style.opacity = 0; + Blockly.DropDownDiv.animateOutTimer_ = setTimeout(function() { + // Finish animation - reset all values to default. + Blockly.DropDownDiv.hideWithoutAnimation(); + }, Blockly.DropDownDiv.ANIMATION_TIME * 1000); + if (Blockly.DropDownDiv.onHide_) { + Blockly.DropDownDiv.onHide_(); + Blockly.DropDownDiv.onHide_ = null; + } +}; + +/** + * Hide the menu, without animation. + */ +Blockly.DropDownDiv.hideWithoutAnimation = function() { + if (!Blockly.DropDownDiv.isVisible()) { + return; + } + Blockly.DropDownDiv.animateOutTimer_ && window.clearTimeout(Blockly.DropDownDiv.animateOutTimer_); + Blockly.DropDownDiv.positionInternal_(); + Blockly.DropDownDiv.clearContent(); + Blockly.DropDownDiv.owner_ = null; + if (Blockly.DropDownDiv.onHide_) { + Blockly.DropDownDiv.onHide_(); + Blockly.DropDownDiv.onHide_ = null; + } +}; + + +/** + * 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). + * @private + */ +Blockly.DropDownDiv.positionInternal_ = function(initialX, initialY, finalX, 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 = ''; + } +}; + +/** + * Repositions the dropdownDiv on window resize. If it doesn't know how to + * calculate the new position, it wll just hide it instead. + */ +Blockly.DropDownDiv.repositionForWindowResize = function() { + // This condition mainly catches the dropdown div when it is being used as a + // dropdown. It is important not to close it in this case because on Android, + // when a field is focused, the soft keyboard opens triggering a window resize + // event and we want the dropdown div to stick around so users can type into it. + if (Blockly.DropDownDiv.owner_) { + var block = Blockly.DropDownDiv.owner_.sourceBlock_; + var scale = block.workspace.scale; + var bBox = { + width: Blockly.DropDownDiv.positionToField_ ? + Blockly.DropDownDiv.owner_.size_.width : block.width, + height: Blockly.DropDownDiv.positionToField_ ? + Blockly.DropDownDiv.owner_.size_.height : block.height + }; + bBox.width *= scale; + bBox.height *= scale; + var position = Blockly.DropDownDiv.positionToField_ ? + Blockly.DropDownDiv.owner_.fieldGroup_.getBoundingClientRect() : + block.getSvgRoot().getBoundingClientRect(); + // If we can fit it, render below the block. + var primaryX = position.left + bBox.width / 2; + var primaryY = position.top + bBox.height; + // If we can't fit it, render above the entire parent block. + var secondaryX = primaryX; + var secondaryY = position.top; + var metrics = Blockly.DropDownDiv.getPositionMetrics( + primaryX, primaryY, secondaryX, secondaryY); + Blockly.DropDownDiv.positionInternal_( + metrics.initialX, metrics.initialY, + metrics.finalX, metrics.finalY); + } else { + Blockly.DropDownDiv.hide(); + } +}; diff --git a/core/inject.js b/core/inject.js index 6a875dbc1..1b619dc9b 100644 --- a/core/inject.js +++ b/core/inject.js @@ -28,6 +28,7 @@ goog.provide('Blockly.inject'); goog.require('Blockly.BlockDragSurfaceSvg'); goog.require('Blockly.Css'); +goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Grid'); goog.require('Blockly.Options'); goog.require('Blockly.utils'); @@ -341,6 +342,7 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface, // The SVG is now fully assembled. Blockly.svgResize(mainWorkspace); Blockly.WidgetDiv.createDom(); + Blockly.DropDownDiv.createDom(); Blockly.Tooltip.createDom(); return mainWorkspace; }; From 763a06a7acedcdcc3ea9ffb7c856cf2d677b43a9 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 8 Feb 2019 15:40:48 -0800 Subject: [PATCH 2/9] Use dropdown div for colour field --- core/blockly.js | 1 + core/css.js | 14 ++++++++++++++ core/dropdowndiv.js | 8 ++++++++ core/field_colour.js | 39 ++++++++++++++++++++++++++++----------- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/core/blockly.js b/core/blockly.js index a7067caa5..f8b21e1e5 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -333,6 +333,7 @@ Blockly.onContextMenu_ = function(e) { Blockly.hideChaff = function(opt_allowToolbox) { Blockly.Tooltip.hide(); Blockly.WidgetDiv.hide(); + Blockly.DropDownDiv.hideWithoutAnimation(); // For now the trashcan flyout always autocloses because it overlays the // trashcan UI (no trashcan to click to close it) var workspace = Blockly.getMainWorkspace(); diff --git a/core/css.js b/core/css.js index db0133b03..5524a3102 100644 --- a/core/css.js +++ b/core/css.js @@ -225,6 +225,20 @@ Blockly.Css.CONTENT = [ 'cursor: pointer;', '}', + '.arrowTop {', + 'border-top: 1px solid;', + 'border-left: 1px solid;', + 'border-top-left-radius: 4px;', + 'border-color: inherit;', + '}', + + '.arrowBottom {', + 'border-bottom: 1px solid;', + 'border-right: 1px solid;', + 'border-bottom-right-radius: 4px;', + 'border-color: inherit;', + '}', + '.blocklyResizeSE {', 'cursor: se-resize;', 'fill: #aaa;', diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index 4445eb545..d2cf86622 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -236,6 +236,9 @@ Blockly.DropDownDiv.showPositionedByField = function(owner, var bBox = {width: owner.size_.width, height: owner.size_.height}; bBox.width *= scale; bBox.height *= scale; + + // bbox.width * scale is not the same as position.right - position.left. + // What's up with that? var position = owner.fieldGroup_.getBoundingClientRect(); // If we can fit it, render below the block. var primaryX = position.left + bBox.width / 2; @@ -455,6 +458,11 @@ Blockly.DropDownDiv.hideWithoutAnimation = function() { * @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); + var div = Blockly.DropDownDiv.DIV_; // First apply initial translation. div.style.left = initialX != null ? initialX + 'px' : ''; diff --git a/core/field_colour.js b/core/field_colour.js index 14dd449f5..02b5cb31b 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -217,25 +217,42 @@ Blockly.FieldColour.prototype.setColumns = function(columns) { * @private */ Blockly.FieldColour.prototype.showEditor_ = function() { - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, - Blockly.FieldColour.widgetDispose_); - // Record viewport dimensions before adding the widget. - var viewportBBox = Blockly.utils.getViewportBBox(); - var anchorBBox = this.getScaledBBox_(); + Blockly.DropDownDiv.hideWithoutAnimation(); + Blockly.DropDownDiv.clearContent(); - // Create and add the colour picker, then record the size. var picker = this.createWidget_(); - Blockly.WidgetDiv.DIV.appendChild(picker); - var paletteSize = goog.style.getSize(picker); + Blockly.DropDownDiv.getContentDiv().appendChild(picker); + Blockly.DropDownDiv.setColour('#ffffff', '#dddddd'); - // Position the picker to line up with the field. - Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, paletteSize, - this.sourceBlock_.RTL); + 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_); + + // Old code is below here + // + // + // Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, + // Blockly.FieldColour.widgetDispose_); + + // // Record viewport dimensions before adding the widget. + // var viewportBBox = Blockly.utils.getViewportBBox(); + // var anchorBBox = this.getScaledBBox_(); + + // // Create and add the colour picker, then record the size. + // var picker = this.createWidget_(); + // Blockly.WidgetDiv.DIV.appendChild(picker); + // var paletteSize = goog.style.getSize(picker); + + // // Position the picker to line up with the field. + // Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, paletteSize, + // this.sourceBlock_.RTL); + + // // Configure event handler on the table to listen for any event in a cell. + // Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker, + // 'mouseup', this, this.onClick_); }; /** From c426aad3879b15dcde2727edce0032571efb2827 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Mon, 11 Feb 2019 11:23:41 -0800 Subject: [PATCH 3/9] Use dropdown div for dropdowns --- core/css.js | 93 +++++++++++++++++++++++++++++------------- core/dropdowndiv.js | 2 +- core/field_dropdown.js | 65 +++++++++++++++++++---------- 3 files changed, 110 insertions(+), 50 deletions(-) diff --git a/core/css.js b/core/css.js index 5524a3102..4b4f3032f 100644 --- a/core/css.js +++ b/core/css.js @@ -190,8 +190,8 @@ Blockly.Css.CONTENT = [ 'z-index: 1000;', 'display: none;', 'border: 1px solid;', - 'border-radius: 4px;', - 'box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3);', + 'border-radius: 1px;', + //'box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3);', 'padding: 4px;', '-webkit-user-select: none;', '}', @@ -643,9 +643,9 @@ Blockly.Css.CONTENT = [ 'max-height: 100%;', '}', - '.blocklyDropdownMenu {', - 'padding: 0 !important;', - '}', + // '.blocklyDropdownMenu {', + // 'padding: 0 !important;', + // '}', /* Override the default Closure URL. */ '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', @@ -822,6 +822,13 @@ Blockly.Css.CONTENT = [ 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ '}', + '.blocklyDropDownDiv .goog-menu {', + 'cursor: default;', + 'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;', + 'outline: none;', + 'z-index: 20000;', /* Arbitrary, but some apps depend on it... */ + '}', + /* Copied from: goog/css/menuitem.css */ /* * Copyright 2009 The Closure Library Authors. All Rights Reserved. @@ -851,19 +858,22 @@ Blockly.Css.CONTENT = [ * rely solely on the BiDi flipping by the CSS compiler. That's why we're * not adding the #noflip to .goog-menuitem. */ - '.blocklyWidgetDiv .goog-menuitem {', + '.blocklyWidgetDiv .goog-menuitem, ', + '.blocklyDropDownDiv .goog-menuitem {', 'color: #000;', 'font: normal 13px Arial, sans-serif;', 'list-style: none;', 'margin: 0;', /* 28px on the left for icon or checkbox; 7em on the right for shortcut. */ - 'padding: 4px 7em 4px 28px;', + 'min-width: 7em;', + 'padding: 5px 5px 5px 28px;', 'white-space: nowrap;', '}', /* BiDi override for the resting state. */ /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem.goog-menuitem-rtl {', + '.blocklyWidgetDiv .goog-menuitem.goog-menuitem-rtl, ', + '.blocklyDropDownDiv .goog-menuitem.goog-menuitem-rtl {', /* Flip left/right padding for BiDi. */ 'padding-left: 7em;', 'padding-right: 28px;', @@ -872,36 +882,43 @@ Blockly.Css.CONTENT = [ /* If a menu doesn't have checkable items or items with icons, * remove padding. */ - '.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,', - '.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem {', + '.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem, ', + '.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem, ', + '.blocklyDropDownDiv .goog-menu-nocheckbox .goog-menuitem, ', + '.blocklyDropDownDiv .goog-menu-noicon .goog-menuitem { ', 'padding-left: 12px;', '}', /* If a menu doesn't have items with shortcuts, leave just enough room for * submenu arrows, if they are rendered. */ - '.blocklyWidgetDiv .goog-menu-noaccel .goog-menuitem {', + '.blocklyWidgetDiv .goog-menu-noaccel .goog-menuitem, ', + '.blocklyDropDownDiv .goog-menu-noaccel .goog-menuitem {', 'padding-right: 20px;', '}', - '.blocklyWidgetDiv .goog-menuitem-content {', + '.blocklyWidgetDiv .goog-menuitem-content, ', + '.blocklyDropDownDiv .goog-menuitem-content {', 'color: #000;', 'font: normal 13px Arial, sans-serif;', '}', /* State: disabled. */ - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-accel,', - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content {', + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-accel, ', + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content, ', + '.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-accel, ', + '.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-content {', 'color: #ccc !important;', '}', - '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon {', + '.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon, ', + '.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-icon {', 'opacity: 0.3;', 'filter: alpha(opacity=30);', '}', /* State: hover. */ - '.blocklyWidgetDiv .goog-menuitem-highlight,', + '.blocklyWidgetDiv .goog-menuitem-highlight, ', '.blocklyWidgetDiv .goog-menuitem-hover {', 'background-color: #d6e9f8;', /* Use an explicit top and bottom border so that the selection is visible', @@ -913,9 +930,16 @@ Blockly.Css.CONTENT = [ 'padding-top: 3px;', '}', + '.blocklyDropDownDiv .goog-menuitem-highlight, ', + '.blocklyDropDownDiv .goog-menuitem-hover {', + 'background-color: rgba(0, 0, 0, 0.2);', + '}', + /* State: selected/checked. */ - '.blocklyWidgetDiv .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-icon {', + '.blocklyWidgetDiv .goog-menuitem-checkbox, ', + '.blocklyWidgetDiv .goog-menuitem-icon, ', + '.blocklyDropDownDiv .goog-menuitem-checkbox, ', + '.blocklyDropDownDiv .goog-menuitem-icon {', 'background-repeat: no-repeat;', 'height: 16px;', 'left: 6px;', @@ -927,21 +951,30 @@ Blockly.Css.CONTENT = [ /* BiDi override for the selected/checked state. */ /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon {', + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox, ', + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon, ', + '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox, ', + '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {', /* Flip left/right positioning. */ 'left: auto;', 'right: 6px;', '}', - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', - '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon {', + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox, ', + '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon, ', + '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox, ', + '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {', /* Client apps may override the URL at which they serve the sprite. */ - 'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', + //'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', + 'background: url(<<>>/sprites.png) no-repeat -48px -16px !important;', + 'position: static;', /* Scroll with the menu. */ + 'float: left;', + 'margin-left: -24px;', '}', /* Keyboard shortcut ("accelerator") style. */ - '.blocklyWidgetDiv .goog-menuitem-accel {', + '.blocklyWidgetDiv .goog-menuitem-accel, ', + '.blocklyDropDownDiv .goog-menuitem-accel {', 'color: #999;', /* Keyboard shortcuts are untranslated; always left-to-right. */ /* #noflip */ @@ -955,7 +988,8 @@ Blockly.Css.CONTENT = [ /* BiDi override for shortcut style. */ /* #noflip */ - '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-accel {', + '.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-accel, ', + '.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-accel {', /* Flip left/right positioning and text alignment. */ 'left: 0;', 'right: auto;', @@ -963,11 +997,13 @@ Blockly.Css.CONTENT = [ '}', /* Mnemonic styles. */ - '.blocklyWidgetDiv .goog-menuitem-mnemonic-hint {', + '.blocklyWidgetDiv .goog-menuitem-mnemonic-hint, ', + '.blocklyDropDownDiv .goog-menuitem-mnemonic-hint {', 'text-decoration: underline;', '}', - '.blocklyWidgetDiv .goog-menuitem-mnemonic-separator {', + '.blocklyWidgetDiv .goog-menuitem-mnemonic-separator, ', + '.blocklyDropDownDiv .goog-menuitem-mnemonic-separator {', 'color: #999;', 'font-size: 12px;', 'padding-left: 4px;', @@ -987,7 +1023,8 @@ Blockly.Css.CONTENT = [ * @author attila@google.com (Attila Bodis) */ - '.blocklyWidgetDiv .goog-menuseparator {', + '.blocklyWidgetDiv .goog-menuseparator, ', + '.blocklyDropDownDiv .goog-menuseparator {', 'border-top: 1px solid #ccc;', 'margin: 4px 0;', 'padding: 0;', diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index d2cf86622..49f1ad8fe 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -93,7 +93,7 @@ Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING = 12; * @type {number} * @const */ -Blockly.DropDownDiv.PADDING_Y = 20; +Blockly.DropDownDiv.PADDING_Y = 8; /** * Length of animations in seconds. diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 1de845765..4df54395a 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -140,7 +140,15 @@ Blockly.FieldDropdown.prototype.init = function() { * @private */ Blockly.FieldDropdown.prototype.showEditor_ = function() { - Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, null); + // consider passing the options into createMenu instead of getting them twice. + var options = this.getOptions(); + if (options.length == 0) return; + + this.dropDownOpen_ = true; + // If there is an existing drop-down someone else owns, hide it immediately and clear it. + Blockly.DropDownDiv.hideWithoutAnimation(); + Blockly.DropDownDiv.clearContent(); + var menu = this.createMenu_(); this.addActionListener_(menu); this.positionMenu_(menu); @@ -154,13 +162,16 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() { Blockly.FieldDropdown.prototype.addActionListener_ = function(menu) { var thisField = this; + var selected = false; function callback(e) { + if (selected) return; var menu = this; var menuItem = e.target; if (menuItem) { thisField.onItemSelected(menu, menuItem); + selected = true; } - Blockly.WidgetDiv.hideIfOwner(thisField); + Blockly.DropDownDiv.hide(); Blockly.Events.setGroup(false); } // Listen for mouse/keyboard events. @@ -205,25 +216,28 @@ 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_(); + // // 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 + // * document.documentElement.clientHeight; + // if (menuSize.height > menuMaxHeightPx) { + // menuSize.height = menuMaxHeightPx; + // } + + // if (this.sourceBlock_.RTL) { + // Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize); + // } + // // Position the menu. + // Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, + // this.sourceBlock_.RTL); this.createWidget_(menu); - var menuSize = Blockly.utils.uiMenu.getSize(menu); - - var menuMaxHeightPx = Blockly.FieldDropdown.MAX_MENU_HEIGHT_VH - * document.documentElement.clientHeight; - if (menuSize.height > menuMaxHeightPx) { - menuSize.height = menuMaxHeightPx; - } - - if (this.sourceBlock_.RTL) { - Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize); - } - // Position the menu. - Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, - this.sourceBlock_.RTL); + this.updateColours_(); + Blockly.DropDownDiv.showPositionedByField(this); // 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. @@ -236,13 +250,22 @@ Blockly.FieldDropdown.prototype.positionMenu_ = function(menu) { * @private */ Blockly.FieldDropdown.prototype.createWidget_ = function(menu) { - var div = Blockly.WidgetDiv.DIV; - menu.render(div); + var contentDiv = Blockly.DropDownDiv.getContentDiv(); + menu.render(contentDiv); Blockly.utils.addClass(menu.getElement(), 'blocklyDropdownMenu'); // Enable autofocus after the initial render to avoid issue #1329. menu.setAllowAutoFocus(true); }; +/** + * Set the colours of the dropdown div to match the colours of the field or + * parent block. + * @private + */ +Blockly.FieldDropdown.prototype.updateColours_ = function() { + Blockly.DropDownDiv.setColour('#ffffff', '#dddddd'); +}; + /** * Returns the coordinates of the anchor rectangle for the widget div. * On a FieldDropdown we take the top-left corner of the field, then adjust for From 2c624c51446a2d5573f4b5e7f4d14d85e3ba72c0 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Tue, 26 Feb 2019 11:03:28 -0800 Subject: [PATCH 4/9] Use dropdowndiv for field colour but not field dropdown --- core/dropdowndiv.js | 19 +++++------- core/field_colour.js | 66 +++++++++++++++++++++++++++--------------- core/field_dropdown.js | 65 ++++++++++++++--------------------------- 3 files changed, 71 insertions(+), 79 deletions(-) diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index 49f1ad8fe..a43b7b0af 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -93,7 +93,7 @@ Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING = 12; * @type {number} * @const */ -Blockly.DropDownDiv.PADDING_Y = 8; +Blockly.DropDownDiv.PADDING_Y = 16; /** * Length of animations in seconds. @@ -232,17 +232,10 @@ Blockly.DropDownDiv.showPositionedByBlock = function(field, block, */ Blockly.DropDownDiv.showPositionedByField = function(owner, opt_onHide, opt_secondaryYOffset) { - var scale = owner.sourceBlock_.workspace.scale; - var bBox = {width: owner.size_.width, height: owner.size_.height}; - bBox.width *= scale; - bBox.height *= scale; - - // bbox.width * scale is not the same as position.right - position.left. - // What's up with that? var position = owner.fieldGroup_.getBoundingClientRect(); // If we can fit it, render below the block. - var primaryX = position.left + bBox.width / 2; - var primaryY = position.top + bBox.height; + var primaryX = position.left + position.width / 2; + var primaryY = position.bottom; // If we can't fit it, render above the entire parent block. var secondaryX = primaryX; var secondaryY = position.top; @@ -251,8 +244,10 @@ Blockly.DropDownDiv.showPositionedByField = function(owner, } // Set bounds to workspace; show the drop-down. Blockly.DropDownDiv.positionToField_ = true; - Blockly.DropDownDiv.setBoundsElement(owner.sourceBlock_.workspace.getParentSvg().parentNode); - return Blockly.DropDownDiv.show(owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide); + Blockly.DropDownDiv.setBoundsElement( + owner.sourceBlock_.workspace.getParentSvg().parentNode); + return Blockly.DropDownDiv.show( + owner, primaryX, primaryY, secondaryX, secondaryY, opt_onHide); }; diff --git a/core/field_colour.js b/core/field_colour.js index 02b5cb31b..633aff461 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -60,6 +60,22 @@ Blockly.FieldColour.fromJson = function(options) { return new Blockly.FieldColour(options['colour']); }; +/** + * Default width of a colour field. + * @type {number} + * @private + * @const + */ +Blockly.FieldColour.DEFAULT_WIDTH = 16; + +/** + * Default height of a colour field. + * @type {number} + * @private + * @const + */ +Blockly.FieldColour.DEFAULT_HEIGHT = 12; + /** * Array of colours used by this field. If null, use the global list. * @type {Array.} @@ -88,6 +104,8 @@ Blockly.FieldColour.prototype.columns_ = 0; Blockly.FieldColour.prototype.init = function() { Blockly.FieldColour.superClass_.init.call(this); this.borderRect_.style['fillOpacity'] = 1; + this.size_ = new goog.math.Size(Blockly.FieldColour.DEFAULT_WIDTH, + Blockly.FieldColour.DEFAULT_HEIGHT); this.setValue(this.getValue()); }; @@ -112,6 +130,31 @@ Blockly.FieldColour.prototype.getValue = function() { return this.colour_; }; +/** + * Get the size, and rerender if necessary. + * @return {!goog.math.Size} Height and width. + */ +Blockly.FieldColour.prototype.getSize = function() { + if (!this.size_.width) { + this.render_(); + } + + return this.size_; +}; + +/** + * Updates the width of the field. Colour fields have a constant width, but + * the width is sometimes reset to force a rerender. + */ +Blockly.FieldColour.prototype.updateWidth = function() { + var width = Blockly.FieldColour.DEFAULT_WIDTH; + if (this.borderRect_) { + this.borderRect_.setAttribute('width', + width + Blockly.BlockSvg.SEP_SPACE_X); + } + this.size_.width = width; +}; + /** * Set the colour. * @param {string} colour The new colour in '#rrggbb' format. @@ -230,29 +273,6 @@ Blockly.FieldColour.prototype.showEditor_ = function() { // Configure event handler on the table to listen for any event in a cell. Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker, 'mouseup', this, this.onClick_); - - // Old code is below here - // - // - // Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, - // Blockly.FieldColour.widgetDispose_); - - // // Record viewport dimensions before adding the widget. - // var viewportBBox = Blockly.utils.getViewportBBox(); - // var anchorBBox = this.getScaledBBox_(); - - // // Create and add the colour picker, then record the size. - // var picker = this.createWidget_(); - // Blockly.WidgetDiv.DIV.appendChild(picker); - // var paletteSize = goog.style.getSize(picker); - - // // Position the picker to line up with the field. - // Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, paletteSize, - // this.sourceBlock_.RTL); - - // // Configure event handler on the table to listen for any event in a cell. - // Blockly.FieldColour.onUpWrapper_ = Blockly.bindEvent_(picker, - // 'mouseup', this, this.onClick_); }; /** diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 4df54395a..1de845765 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -140,15 +140,7 @@ Blockly.FieldDropdown.prototype.init = function() { * @private */ Blockly.FieldDropdown.prototype.showEditor_ = function() { - // consider passing the options into createMenu instead of getting them twice. - var options = this.getOptions(); - if (options.length == 0) return; - - this.dropDownOpen_ = true; - // If there is an existing drop-down someone else owns, hide it immediately and clear it. - Blockly.DropDownDiv.hideWithoutAnimation(); - Blockly.DropDownDiv.clearContent(); - + Blockly.WidgetDiv.show(this, this.sourceBlock_.RTL, null); var menu = this.createMenu_(); this.addActionListener_(menu); this.positionMenu_(menu); @@ -162,16 +154,13 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() { Blockly.FieldDropdown.prototype.addActionListener_ = function(menu) { var thisField = this; - var selected = false; function callback(e) { - if (selected) return; var menu = this; var menuItem = e.target; if (menuItem) { thisField.onItemSelected(menu, menuItem); - selected = true; } - Blockly.DropDownDiv.hide(); + Blockly.WidgetDiv.hideIfOwner(thisField); Blockly.Events.setGroup(false); } // Listen for mouse/keyboard events. @@ -216,28 +205,25 @@ 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_(); + // 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 - // * document.documentElement.clientHeight; - // if (menuSize.height > menuMaxHeightPx) { - // menuSize.height = menuMaxHeightPx; - // } - - // if (this.sourceBlock_.RTL) { - // Blockly.utils.uiMenu.adjustBBoxesForRTL(viewportBBox, anchorBBox, menuSize); - // } - // // Position the menu. - // Blockly.WidgetDiv.positionWithAnchor(viewportBBox, anchorBBox, menuSize, - // this.sourceBlock_.RTL); this.createWidget_(menu); - this.updateColours_(); - Blockly.DropDownDiv.showPositionedByField(this); + var menuSize = Blockly.utils.uiMenu.getSize(menu); + + var menuMaxHeightPx = Blockly.FieldDropdown.MAX_MENU_HEIGHT_VH + * document.documentElement.clientHeight; + if (menuSize.height > menuMaxHeightPx) { + menuSize.height = menuMaxHeightPx; + } + + 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. @@ -250,22 +236,13 @@ Blockly.FieldDropdown.prototype.positionMenu_ = function(menu) { * @private */ Blockly.FieldDropdown.prototype.createWidget_ = function(menu) { - var contentDiv = Blockly.DropDownDiv.getContentDiv(); - menu.render(contentDiv); + var div = Blockly.WidgetDiv.DIV; + menu.render(div); Blockly.utils.addClass(menu.getElement(), 'blocklyDropdownMenu'); // Enable autofocus after the initial render to avoid issue #1329. menu.setAllowAutoFocus(true); }; -/** - * Set the colours of the dropdown div to match the colours of the field or - * parent block. - * @private - */ -Blockly.FieldDropdown.prototype.updateColours_ = function() { - Blockly.DropDownDiv.setColour('#ffffff', '#dddddd'); -}; - /** * Returns the coordinates of the anchor rectangle for the widget div. * On a FieldDropdown we take the top-left corner of the field, then adjust for From 4f3bb673ef4f2fab856e2f36099ed4381b9d7702 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Tue, 26 Feb 2019 11:05:27 -0800 Subject: [PATCH 5/9] Add require and rebuild for travis --- blockly_uncompressed.js | 6 ++++-- core/field_colour.js | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 1b8be16da..a71db4ce3 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -52,13 +52,14 @@ goog.addDependency("../../../" + dir + "/core/constants.js", ['Blockly.constants goog.addDependency("../../../" + dir + "/core/contextmenu.js", ['Blockly.ContextMenu'], ['Blockly.Events.BlockCreate', 'Blockly.utils', 'Blockly.utils.uiMenu', 'Blockly.Xml', 'goog.events', 'goog.math.Coordinate', 'goog.ui.Menu', 'goog.ui.MenuItem', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/css.js", ['Blockly.Css'], []); goog.addDependency("../../../" + dir + "/core/dragged_connection_manager.js", ['Blockly.DraggedConnectionManager'], ['Blockly.BlockAnimations', 'Blockly.RenderedConnection', 'goog.math.Coordinate']); +goog.addDependency("../../../" + dir + "/core/dropdowndiv.js", ['Blockly.DropDownDiv'], ['goog.dom', 'goog.style']); goog.addDependency("../../../" + dir + "/core/events.js", ['Blockly.Events'], ['Blockly.utils']); goog.addDependency("../../../" + dir + "/core/events_abstract.js", ['Blockly.Events.Abstract'], ['Blockly.Events']); goog.addDependency("../../../" + dir + "/core/extensions.js", ['Blockly.Extensions'], ['Blockly.Mutator', 'Blockly.utils']); goog.addDependency("../../../" + dir + "/core/field.js", ['Blockly.Field'], ['Blockly.Events.BlockChange', 'Blockly.Gesture', 'Blockly.utils', 'goog.math.Size', 'goog.style', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/field_angle.js", ['Blockly.FieldAngle'], ['Blockly.FieldTextInput', 'Blockly.utils', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/field_checkbox.js", ['Blockly.FieldCheckbox'], ['Blockly.Field', 'Blockly.utils']); -goog.addDependency("../../../" + dir + "/core/field_colour.js", ['Blockly.FieldColour'], ['Blockly.Field', 'Blockly.utils', 'goog.style']); +goog.addDependency("../../../" + dir + "/core/field_colour.js", ['Blockly.FieldColour'], ['Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.utils', 'goog.style']); goog.addDependency("../../../" + dir + "/core/field_date.js", ['Blockly.FieldDate'], ['Blockly.Field', 'Blockly.utils', 'goog.date', 'goog.date.DateTime', 'goog.events', 'goog.i18n.DateTimeSymbols', 'goog.i18n.DateTimeSymbols_he', 'goog.style', 'goog.ui.DatePicker']); goog.addDependency("../../../" + dir + "/core/field_dropdown.js", ['Blockly.FieldDropdown'], ['Blockly.Field', 'Blockly.utils', 'Blockly.utils.uiMenu', 'goog.events', 'goog.ui.Menu', 'goog.ui.MenuItem', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/field_image.js", ['Blockly.FieldImage'], ['Blockly.Field', 'Blockly.utils', 'goog.math.Size']); @@ -75,7 +76,7 @@ goog.addDependency("../../../" + dir + "/core/generator.js", ['Blockly.Generator goog.addDependency("../../../" + dir + "/core/gesture.js", ['Blockly.Gesture'], ['Blockly.BlockAnimations', 'Blockly.BlockDragger', 'Blockly.BubbleDragger', 'Blockly.constants', 'Blockly.Events.Ui', 'Blockly.FlyoutDragger', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.utils', 'Blockly.WorkspaceDragger', 'goog.math.Coordinate']); goog.addDependency("../../../" + dir + "/core/grid.js", ['Blockly.Grid'], ['Blockly.utils', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/icon.js", ['Blockly.Icon'], ['Blockly.utils', 'goog.math.Coordinate']); -goog.addDependency("../../../" + dir + "/core/inject.js", ['Blockly.inject'], ['Blockly.BlockDragSurfaceSvg', 'Blockly.Css', 'Blockly.Grid', 'Blockly.Options', 'Blockly.utils', 'Blockly.WorkspaceSvg', 'Blockly.WorkspaceDragSurfaceSvg', 'goog.ui.Component', 'goog.userAgent']); +goog.addDependency("../../../" + dir + "/core/inject.js", ['Blockly.inject'], ['Blockly.BlockDragSurfaceSvg', 'Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Grid', 'Blockly.Options', 'Blockly.utils', 'Blockly.WorkspaceSvg', 'Blockly.WorkspaceDragSurfaceSvg', 'goog.ui.Component', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/input.js", ['Blockly.Input'], ['Blockly.Connection', 'Blockly.FieldLabel']); goog.addDependency("../../../" + dir + "/core/insertion_marker_manager.js", ['Blockly.InsertionMarkerManager'], ['Blockly.BlockAnimations', 'Blockly.Events.BlockMove', 'Blockly.RenderedConnection', 'goog.math.Coordinate']); goog.addDependency("../../../" + dir + "/core/msg.js", ['Blockly.Msg'], []); @@ -1759,6 +1760,7 @@ goog.require('Blockly.ConnectionDB'); goog.require('Blockly.ContextMenu'); goog.require('Blockly.Css'); goog.require('Blockly.DraggedConnectionManager'); +goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Events'); goog.require('Blockly.Events.Abstract'); goog.require('Blockly.Events.BlockBase'); diff --git a/core/field_colour.js b/core/field_colour.js index 633aff461..9075543cc 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -26,6 +26,7 @@ goog.provide('Blockly.FieldColour'); +goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Field'); goog.require('Blockly.utils'); From 58027b9668e948b6caa360091a7cab4c69ab9470 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Tue, 26 Feb 2019 11:10:55 -0800 Subject: [PATCH 6/9] css cleanup --- core/css.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/css.js b/core/css.js index 4b4f3032f..ff7230396 100644 --- a/core/css.js +++ b/core/css.js @@ -191,7 +191,6 @@ Blockly.Css.CONTENT = [ 'display: none;', 'border: 1px solid;', 'border-radius: 1px;', - //'box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, .3);', 'padding: 4px;', '-webkit-user-select: none;', '}', @@ -643,9 +642,9 @@ Blockly.Css.CONTENT = [ 'max-height: 100%;', '}', - // '.blocklyDropdownMenu {', - // 'padding: 0 !important;', - // '}', + '.blocklyDropdownMenu {', + 'padding: 0 !important;', + '}', /* Override the default Closure URL. */ '.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,', @@ -965,8 +964,7 @@ Blockly.Css.CONTENT = [ '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox, ', '.blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {', /* Client apps may override the URL at which they serve the sprite. */ - //'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', - 'background: url(<<>>/sprites.png) no-repeat -48px -16px !important;', + 'background: url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0;', 'position: static;', /* Scroll with the menu. */ 'float: left;', 'margin-left: -24px;', From dfc74b8c4ffc2e6fe8417e834a68149ec3aaf1e4 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Tue, 26 Feb 2019 14:54:21 -0800 Subject: [PATCH 7/9] Use dropdownDiv for the angle field as well --- blockly_uncompressed.js | 4 ++-- core/css.js | 2 +- core/field_angle.js | 31 ++++++++++++++++++++++++------- core/field_textinput.js | 4 ++++ tests/blocks/test_blocks.js | 3 ++- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index a71db4ce3..55e6ce2c6 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -57,7 +57,7 @@ goog.addDependency("../../../" + dir + "/core/events.js", ['Blockly.Events'], [' goog.addDependency("../../../" + dir + "/core/events_abstract.js", ['Blockly.Events.Abstract'], ['Blockly.Events']); goog.addDependency("../../../" + dir + "/core/extensions.js", ['Blockly.Extensions'], ['Blockly.Mutator', 'Blockly.utils']); goog.addDependency("../../../" + dir + "/core/field.js", ['Blockly.Field'], ['Blockly.Events.BlockChange', 'Blockly.Gesture', 'Blockly.utils', 'goog.math.Size', 'goog.style', 'goog.userAgent']); -goog.addDependency("../../../" + dir + "/core/field_angle.js", ['Blockly.FieldAngle'], ['Blockly.FieldTextInput', 'Blockly.utils', 'goog.userAgent']); +goog.addDependency("../../../" + dir + "/core/field_angle.js", ['Blockly.FieldAngle'], ['Blockly.DropDownDiv', 'Blockly.FieldTextInput', 'Blockly.utils', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/field_checkbox.js", ['Blockly.FieldCheckbox'], ['Blockly.Field', 'Blockly.utils']); goog.addDependency("../../../" + dir + "/core/field_colour.js", ['Blockly.FieldColour'], ['Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.utils', 'goog.style']); goog.addDependency("../../../" + dir + "/core/field_date.js", ['Blockly.FieldDate'], ['Blockly.Field', 'Blockly.utils', 'goog.date', 'goog.date.DateTime', 'goog.events', 'goog.i18n.DateTimeSymbols', 'goog.i18n.DateTimeSymbols_he', 'goog.style', 'goog.ui.DatePicker']); @@ -65,7 +65,7 @@ goog.addDependency("../../../" + dir + "/core/field_dropdown.js", ['Blockly.Fiel goog.addDependency("../../../" + dir + "/core/field_image.js", ['Blockly.FieldImage'], ['Blockly.Field', 'Blockly.utils', 'goog.math.Size']); goog.addDependency("../../../" + dir + "/core/field_label.js", ['Blockly.FieldLabel'], ['Blockly.Field', 'Blockly.Tooltip', 'Blockly.utils', 'goog.math.Size']); goog.addDependency("../../../" + dir + "/core/field_number.js", ['Blockly.FieldNumber'], ['Blockly.FieldTextInput']); -goog.addDependency("../../../" + dir + "/core/field_textinput.js", ['Blockly.FieldTextInput'], ['Blockly.Field', 'Blockly.Msg', 'Blockly.utils', 'goog.math.Coordinate', 'goog.userAgent']); +goog.addDependency("../../../" + dir + "/core/field_textinput.js", ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Field', 'Blockly.Msg', 'Blockly.utils', 'goog.math.Coordinate', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/field_variable.js", ['Blockly.FieldVariable'], ['Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.utils', 'Blockly.VariableModel', 'Blockly.Variables', 'goog.math.Size']); goog.addDependency("../../../" + dir + "/core/flyout_base.js", ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutButton', 'Blockly.Gesture', 'Blockly.Touch', 'Blockly.utils', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'goog.math.Rect']); goog.addDependency("../../../" + dir + "/core/flyout_button.js", ['Blockly.FlyoutButton'], ['Blockly.utils', 'goog.math.Coordinate']); diff --git a/core/css.js b/core/css.js index ff7230396..d689d2d38 100644 --- a/core/css.js +++ b/core/css.js @@ -190,7 +190,7 @@ Blockly.Css.CONTENT = [ 'z-index: 1000;', 'display: none;', 'border: 1px solid;', - 'border-radius: 1px;', + 'border-radius: 2px;', 'padding: 4px;', '-webkit-user-select: none;', '}', diff --git a/core/field_angle.js b/core/field_angle.js index a4ef08a2f..d612effb8 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -26,6 +26,7 @@ goog.provide('Blockly.FieldAngle'); +goog.require('Blockly.DropDownDiv'); goog.require('Blockly.FieldTextInput'); goog.require('Blockly.utils'); @@ -165,11 +166,12 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD; // Mobile browsers have issues with in-line textareas (focus & keyboards). Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); - var div = Blockly.WidgetDiv.DIV; - if (!div.firstChild) { - // Mobile interface uses Blockly.prompt. - return; - } + + // If there is an existing drop-down someone else owns, hide it immediately and clear it. + Blockly.DropDownDiv.hideWithoutAnimation(); + Blockly.DropDownDiv.clearContent(); + var div = Blockly.DropDownDiv.getContentDiv(); + // Build the SVG DOM. var svg = Blockly.utils.createSvgElement('svg', { 'xmlns': 'http://www.w3.org/2000/svg', @@ -204,14 +206,17 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { Blockly.FieldAngle.HALF + ',' + Blockly.FieldAngle.HALF + ')' }, svg); } - svg.style.marginLeft = (15 - Blockly.FieldAngle.RADIUS) + 'px'; + + Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), + this.sourceBlock_.getColour()); + 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 behavior. For now, using bindEvent_ instead of // bindEventWithChecks_ allows it to work without a mousedown/touchstart. this.clickWrapper_ = - Blockly.bindEvent_(svg, 'click', this, Blockly.WidgetDiv.hide); + Blockly.bindEvent_(svg, 'click', this, this.hide_.bind(this)); this.moveWrapper1_ = Blockly.bindEvent_(circle, 'mousemove', this, this.onMouseMove); this.moveWrapper2_ = @@ -219,6 +224,18 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { this.updateGraph_(); }; +/** + * Hide the editor and unbind event listeners. + * @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(); +}; + /** * Set the angle to match the mouse's position. * @param {!Event} e Mouse move event. diff --git a/core/field_textinput.js b/core/field_textinput.js index d727603fe..a0d3897a9 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -26,6 +26,7 @@ goog.provide('Blockly.FieldTextInput'); +goog.require('Blockly.DropDownDiv'); goog.require('Blockly.Field'); goog.require('Blockly.Msg'); goog.require('Blockly.utils'); @@ -260,11 +261,14 @@ 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) { htmlInput.value = 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(); } diff --git a/tests/blocks/test_blocks.js b/tests/blocks/test_blocks.js index 267b87abe..3782ce91a 100644 --- a/tests/blocks/test_blocks.js +++ b/tests/blocks/test_blocks.js @@ -173,7 +173,8 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT "text": "NO ANGLE FIELD" } } - ] + ], + "colour": "230" }, { "type": "example_date", From 0b1bd78674a96952cf64a733f0160300933e5b2c Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 28 Feb 2019 15:11:57 -0800 Subject: [PATCH 8/9] Clarify some dropdown rendering code --- blockly_uncompressed.js | 2 +- core/dropdowndiv.js | 89 +++++++++++++++++++++++++---------------- core/field_colour.js | 4 +- core/utils.js | 17 ++++++++ 4 files changed, 75 insertions(+), 37 deletions(-) diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 55e6ce2c6..c69b0ff60 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -52,7 +52,7 @@ goog.addDependency("../../../" + dir + "/core/constants.js", ['Blockly.constants goog.addDependency("../../../" + dir + "/core/contextmenu.js", ['Blockly.ContextMenu'], ['Blockly.Events.BlockCreate', 'Blockly.utils', 'Blockly.utils.uiMenu', 'Blockly.Xml', 'goog.events', 'goog.math.Coordinate', 'goog.ui.Menu', 'goog.ui.MenuItem', 'goog.userAgent']); goog.addDependency("../../../" + dir + "/core/css.js", ['Blockly.Css'], []); goog.addDependency("../../../" + dir + "/core/dragged_connection_manager.js", ['Blockly.DraggedConnectionManager'], ['Blockly.BlockAnimations', 'Blockly.RenderedConnection', 'goog.math.Coordinate']); -goog.addDependency("../../../" + dir + "/core/dropdowndiv.js", ['Blockly.DropDownDiv'], ['goog.dom', 'goog.style']); +goog.addDependency("../../../" + dir + "/core/dropdowndiv.js", ['Blockly.DropDownDiv'], ['Blockly.utils', 'goog.dom', 'goog.style']); goog.addDependency("../../../" + dir + "/core/events.js", ['Blockly.Events'], ['Blockly.utils']); goog.addDependency("../../../" + dir + "/core/events_abstract.js", ['Blockly.Events.Abstract'], ['Blockly.Events']); goog.addDependency("../../../" + dir + "/core/extensions.js", ['Blockly.Extensions'], ['Blockly.Mutator', 'Blockly.utils']); diff --git a/core/dropdowndiv.js b/core/dropdowndiv.js index a43b7b0af..3186c3726 100644 --- a/core/dropdowndiv.js +++ b/core/dropdowndiv.js @@ -28,6 +28,8 @@ goog.provide('Blockly.DropDownDiv'); +goog.require('Blockly.utils'); + goog.require('goog.dom'); goog.require('goog.style'); @@ -117,7 +119,6 @@ Blockly.DropDownDiv.onHide_ = 0; /** * Create and insert the DOM element for this div. - * @param {Element} container Element that the div should be contained in. */ Blockly.DropDownDiv.createDom = function() { if (Blockly.DropDownDiv.DIV_) { @@ -147,7 +148,7 @@ Blockly.DropDownDiv.createDom = function() { /** * Set an element to maintain bounds within. Drop-downs will appear * within the box of this element if possible. - * @param {Element} boundsElement Element to bound drop-down to. + * @param {Element} boundsElement Element to bind drop-down to. */ Blockly.DropDownDiv.setBoundsElement = function(boundsElement) { Blockly.DropDownDiv.boundsElement_ = boundsElement; @@ -294,6 +295,26 @@ Blockly.DropDownDiv.show = function(owner, primaryX, primaryY, secondaryX, secon return metrics.arrowAtTop; }; +/** + * Get sizing info about the bounding element. + * @return {!Object} an object containing size information about the bounding + * element (bounding box and width/height). + * @private + */ +Blockly.DropDownDiv.getBoundsInfo_ = function() { + var boundPosition = Blockly.DropDownDiv.boundsElement_.getBoundingClientRect(); + var boundSize = goog.style.getSize(Blockly.DropDownDiv.boundsElement_); + + return { + left: boundPosition.left, + right: boundPosition.left + boundSize.width, + top: boundPosition.top, + bottom: boundPosition.top + boundSize.height, + width: boundSize.width, + height: boundSize.height + }; +}; + /** * Helper to position the drop-down and the arrow, maintaining bounds. * See explanation of origin points in Blockly.DropDownDiv.show. @@ -304,10 +325,8 @@ Blockly.DropDownDiv.show = function(owner, primaryX, primaryY, secondaryX, secon * @returns {Object} Various final metrics, including rendered positions for drop-down and arrow. */ Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX, secondaryY) { + var boundsInfo = Blockly.DropDownDiv.getBoundsInfo_(); var div = Blockly.DropDownDiv.DIV_; - var boundPosition = Blockly.DropDownDiv.boundsElement_.getBoundingClientRect(); - - var boundSize = goog.style.getSize(Blockly.DropDownDiv.boundsElement_); var divSize = goog.style.getSize(div); // First decide if we will render at primary or secondary position @@ -315,9 +334,9 @@ Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX // renderX, renderY will eventually be the final rendered position of the box. var renderX, renderY, renderedSecondary, renderedTertiary; // Can the div fit inside the bounds if we render below the primary point? - if (primaryY + divSize.height > boundPosition.top + boundSize.height) { + if (primaryY + divSize.height > boundsInfo.bottom) { // We can't fit below in terms of y. Can we fit above? - if (secondaryY - divSize.height < boundPosition.top) { + if (secondaryY - divSize.height < boundsInfo.top) { // We also can't fit above, so just render at the top of the screen. renderX = primaryX; renderY = 0; @@ -335,38 +354,38 @@ Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX renderY = primaryY + Blockly.DropDownDiv.PADDING_Y; renderedSecondary = false; } - // First calculate the absolute arrow X - // This needs to be done before positioning the div, since the arrow - // wants to be as close to the origin point as possible. - var arrowX = renderX - Blockly.DropDownDiv.ARROW_SIZE / 2; - // Keep in overall bounds - arrowX = Math.max(boundPosition.left, Math.min(arrowX, boundPosition.left + boundSize.width)); - // Adjust the x-position of the drop-down so that the div is centered and within bounds. - var centerX = divSize.width / 2; - renderX -= centerX; + var centerX = renderX; + // The dropdown's X position is at the top-left of the dropdown rect, but the + // dropdown should appear centered relative to the desired origin point. + renderX -= divSize.width / 2; // Fit horizontally in the bounds. - renderX = Math.max( - boundPosition.left, - Math.min(renderX, boundPosition.left + boundSize.width - divSize.width) - ); - // After we've finished caclulating renderX, adjust the arrow to be relative to it. - arrowX -= renderX; + renderX = Blockly.utils.clampNumber( + boundsInfo.left, renderX, boundsInfo.right - divSize.width); - // Pad the arrow by some pixels, primarily so that it doesn't render on top of a rounded border. - arrowX = Math.max( + // Calculate the absolute arrow X. The arrow wants to be as close to the + // origin point as possible. The arrow may not be centered in the dropdown div. + var absoluteArrowX = centerX - Blockly.DropDownDiv.ARROW_SIZE / 2; + // Keep in overall bounds + absoluteArrowX = Blockly.utils.clampNumber( + boundsInfo.left, absoluteArrowX, boundsInfo.right); + + // Convert the arrow position to be relative to the top left corner of the div. + var relativeArrowX = absoluteArrowX - renderX; + + // Pad the arrow by some pixels, primarily so that it doesn't render on top + // of a rounded border. + relativeArrowX = Blockly.utils.clampNumber( Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING, - Math.min(arrowX, divSize.width - - Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING - Blockly.DropDownDiv.ARROW_SIZE) - ); + relativeArrowX, + divSize.width - Blockly.DropDownDiv.ARROW_HORIZONTAL_PADDING - + Blockly.DropDownDiv.ARROW_SIZE); - // Calculate arrow Y. If we rendered secondary, add on bottom. - // Extra pixels are added so that it covers the border of the div. - var arrowY = (renderedSecondary) ? divSize.height - Blockly.DropDownDiv.BORDER_SIZE : 0; - arrowY -= (Blockly.DropDownDiv.ARROW_SIZE / 2) + Blockly.DropDownDiv.BORDER_SIZE; + var arrowY = (renderedSecondary) ? + divSize.height - Blockly.DropDownDiv.BORDER_SIZE : 0; + arrowY -= (Blockly.DropDownDiv.ARROW_SIZE / 2) + + Blockly.DropDownDiv.BORDER_SIZE; - // Initial position calculated without any padding to provide an animation point. - var initialX = renderX; // X position remains constant during animation. var initialY; if (renderedSecondary) { initialY = secondaryY - divSize.height; // No padding on Y @@ -375,11 +394,11 @@ Blockly.DropDownDiv.getPositionMetrics = function(primaryX, primaryY, secondaryX } return { - initialX: initialX, + initialX: renderX, // X position remains constant during animation. initialY : initialY, finalX: renderX, finalY: renderY, - arrowX: arrowX, + arrowX: relativeArrowX, arrowY: arrowY, arrowAtTop: !renderedSecondary, arrowVisible: !renderedTertiary diff --git a/core/field_colour.js b/core/field_colour.js index 9075543cc..62fc44a73 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -267,7 +267,9 @@ Blockly.FieldColour.prototype.showEditor_ = function() { var picker = this.createWidget_(); Blockly.DropDownDiv.getContentDiv().appendChild(picker); - Blockly.DropDownDiv.setColour('#ffffff', '#dddddd'); + // Dropdown div accepts CSS colours. + // But maybe this should be done with a class instead? + Blockly.DropDownDiv.setColour('white', 'silver'); Blockly.DropDownDiv.showPositionedByField(this); diff --git a/core/utils.js b/core/utils.js index 2ec39feee..7c98b703c 100644 --- a/core/utils.js +++ b/core/utils.js @@ -1003,3 +1003,20 @@ Blockly.utils.getBlockTypeCounts = function(block, opt_stripFollowing) { } return typeCountsMap; }; + +/** + * Clamp the provided number between the lower bound and the upper bound. + * @param {Number} lowerBound The desired lower bound. + * @param {Number} number The number to clamp. + * @param {Number} upperBound The desired upper bound. + * @return {Number} the clamped number + * @package + */ +Blockly.utils.clampNumber = function(lowerBound, number, upperBound) { + if (upperBound < lowerBound) { + var temp = upperBound; + upperBound = lowerBound; + lowerBound = temp; + } + return Math.max(lowerBound, Math.min(number, upperBound)); +}; From e20a7ef3c2c8b5a07cebe2f3f697d259c608d777 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 1 Mar 2019 12:38:37 -0800 Subject: [PATCH 9/9] Use constants for dropdown div colours in the colour field --- core/field_colour.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/core/field_colour.js b/core/field_colour.js index 62fc44a73..54bfc532e 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -99,6 +99,22 @@ Blockly.FieldColour.prototype.titles_ = null; */ Blockly.FieldColour.prototype.columns_ = 0; +/** + * Border colour for the dropdown div showing the colour picker. Must be a CSS + * string. + * @type {string} + * @private + */ +Blockly.FieldColour.prototype.DROPDOWN_BORDER_COLOUR = 'silver'; + +/** + * Background colour for the dropdown div showing the colour picker. Must be a + * CSS string. + * @type {string} + * @private + */ +Blockly.FieldColour.prototype.DROPDOWN_BACKGROUND_COLOUR = 'white'; + /** * Install this field on a block. */ @@ -267,9 +283,8 @@ Blockly.FieldColour.prototype.showEditor_ = function() { var picker = this.createWidget_(); Blockly.DropDownDiv.getContentDiv().appendChild(picker); - // Dropdown div accepts CSS colours. - // But maybe this should be done with a class instead? - Blockly.DropDownDiv.setColour('white', 'silver'); + Blockly.DropDownDiv.setColour( + this.DROPDOWN_BACKGROUND_COLOUR, this.DROPDOWN_BORDER_COLOUR); Blockly.DropDownDiv.showPositionedByField(this);