Skip to content

Commit

Permalink
fix: do not hide all chaff when resizing
Browse files Browse the repository at this point in the history
  • Loading branch information
maribethb committed Apr 13, 2023
1 parent 5f8330e commit d0f5aac
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 15 deletions.
22 changes: 12 additions & 10 deletions core/bump_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,24 @@ import type {WorkspaceSvg} from './workspace_svg.js';
* Bumps the given object that has passed out of bounds.
*
* @param workspace The workspace containing the object.
* @param scrollMetrics Scroll metrics
* in workspace coordinates.
* @param bounds The region to bump an object into. For example, pass
* ScrollMetrics to bump a block into the scrollable region of the
* workspace, or pass ViewMetrics to bump a block into the visible region of
* the workspace. This should be specified in workspace coordinates.
* @param object The object to bump.
* @returns True if block was bumped.
* @returns True if object was bumped.
*/
function bumpObjectIntoBounds(
workspace: WorkspaceSvg, scrollMetrics: ContainerRegion,
workspace: WorkspaceSvg, bounds: ContainerRegion,
object: IBoundedElement): boolean {
// Compute new top/left position for object.
const objectMetrics = object.getBoundingRectangle();
const height = objectMetrics.bottom - objectMetrics.top;
const width = objectMetrics.right - objectMetrics.left;

const topClamp = scrollMetrics.top;
const scrollMetricsBottom = scrollMetrics.top + scrollMetrics.height;
const bottomClamp = scrollMetricsBottom - height;
const topClamp = bounds.top;
const boundsBottom = bounds.top + bounds.height;
const bottomClamp = boundsBottom - height;
// If the object is taller than the workspace we want to
// top-align the block
const newYPosition =
Expand All @@ -50,9 +52,9 @@ function bumpObjectIntoBounds(

// Note: Even in RTL mode the "anchor" of the object is the
// top-left corner of the object.
let leftClamp = scrollMetrics.left;
const scrollMetricsRight = scrollMetrics.left + scrollMetrics.width;
let rightClamp = scrollMetricsRight - width;
let leftClamp = bounds.left;
const boundsRight = bounds.left + bounds.width;
let rightClamp = boundsRight - width;
if (workspace.RTL) {
// If the object is wider than the workspace and we're in RTL
// mode we want to right-align the block, which means setting
Expand Down
22 changes: 22 additions & 0 deletions core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,28 @@ export abstract class Field<T = any> implements IASTNodeLocationSvg,
protected showEditor_(_e?: Event): void {}
// NOP

/**
* A developer hook to reposition the WidgetDiv during a window resize. You
* need to define this hook if your field has a WidgetDiv that needs to
* reposition itself when the window is resized. For example, text input
* fields define this hook so that the input WidgetDiv can reposition itself
* on a window resize event. This is especially important when modal inputs
* have been disabled, as Android devices will fire a window resize event when
* the soft keyboard opens.
*
* If you want the WidgetDiv to hide itself instead of repositioning, return
* false. This is the default behavior.
*
* DropdownDivs already handle their own positioning logic, so you do not need
* to override this function if your field only has a DropdownDiv.
*
* @returns True if the field should be repositioned,
* false if the WidgetDiv should hide itself instead.
*/
repositionForWindowResize(): boolean {
return false;
}

/**
* Updates the size of the field based on the text.
*
Expand Down
26 changes: 25 additions & 1 deletion core/field_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ goog.declareModuleId('Blockly.FieldInput');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';

import type {BlockSvg} from './block_svg.js';
import {BlockSvg} from './block_svg.js';
import * as bumpObjects from './bump_objects.js';
import * as browserEvents from './browser_events.js';
import * as dialog from './dialog.js';
import * as dom from './utils/dom.js';
Expand Down Expand Up @@ -505,6 +506,29 @@ export abstract class FieldInput<T extends InputTypes> extends Field<string|T> {
div!.style.top = xy.y + 'px';
}

/**
* Handles repositioning the WidgetDiv used for input fields when the
* workspace is resized. Will bump the block into the viewport and update the
* position of the field if necessary.
*
* @returns True for rendered workspaces, as we never want to hide the widget
* div.
*/
override repositionForWindowResize(): boolean {
const block = this.getSourceBlock();
// This shouldn't be possible. We should never have a WidgetDiv if not using
// rendered blocks.
if (!(block instanceof BlockSvg)) return false;

bumpObjects.bumpIntoBounds(
this.workspace_!,
this.workspace_!.getMetricsManager().getViewMetrics(true), block);

this.resizeEditor_();

return true;
}

/**
* Returns whether or not the field is tab navigable.
*
Expand Down
7 changes: 6 additions & 1 deletion core/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,12 @@ function init(mainWorkspace: WorkspaceSvg) {

const workspaceResizeHandler =
browserEvents.conditionalBind(window, 'resize', null, function() {
mainWorkspace.hideChaff(true);
// Don't hide all the chaff. Leave the dropdown and widget divs open if
// possible.
Tooltip.hide();
mainWorkspace.hideComponents(true);
dropDownDiv.repositionForWindowResize();
WidgetDiv.repositionForWindowResize();
common.svgResize(mainWorkspace);
bumpObjects.bumpTopObjectsIntoBounds(mainWorkspace);
});
Expand Down
23 changes: 23 additions & 0 deletions core/widgetdiv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ goog.declareModuleId('Blockly.WidgetDiv');

import * as common from './common.js';
import * as dom from './utils/dom.js';
import type {Field} from './field.js';
import type {Rect} from './utils/rect.js';
import type {Size} from './utils/size.js';
import type {WorkspaceSvg} from './workspace_svg.js';
Expand Down Expand Up @@ -237,3 +238,25 @@ function calculateY(
return anchorBBox.bottom;
}
}

/**
* Determine if the owner is a field for purposes of repositioning.
* We can't simply check `instanceof Field` as that would introduce a circular
* dependency.
*/
function isRepositionable(item: any): item is Field {
return !!item?.repositionForWindowResize;
}

/**
* Reposition the widget div if the owner of it says to.
* If the owner isn't a field, just give up and hide it.
*/
export function repositionForWindowResize(): void {
if (!isRepositionable(owner) || !owner.repositionForWindowResize()) {
// If the owner is not a Field, or if the owner returns false from the
// reposition method, we should hide the widget div. Otherwise, we'll assume
// the owner handled any needed resize.
hide();
}
}
17 changes: 14 additions & 3 deletions core/workspace_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2511,14 +2511,25 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg {
/**
* Close tooltips, context menus, dropdown selections, etc.
*
* @param opt_onlyClosePopups Whether only popups should be closed.
* @param onlyClosePopups Whether only popups should be closed. Defaults to
* false.
*/
hideChaff(opt_onlyClosePopups?: boolean) {
hideChaff(onlyClosePopups = false) {
Tooltip.hide();
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();

const onlyClosePopups = !!opt_onlyClosePopups;
this.hideComponents(onlyClosePopups);
}

/**
* Hide any autohideable components (like flyout, trashcan, and any
* user-registered components).
*
* @param onlyClosePopups Whether only popups should be closed. Defaults to
* false.
*/
hideComponents(onlyClosePopups = false) {
const autoHideables = this.getComponentManager().getComponents(
ComponentManager.Capability.AUTOHIDEABLE, true);
autoHideables.forEach(
Expand Down

0 comments on commit d0f5aac

Please sign in to comment.