Split flyout into horizontal and vertical classes

This commit is contained in:
Rachel Fenichel 2016-09-08 13:45:47 -07:00
parent 79013d14fb
commit a75c07a158
3 changed files with 1717 additions and 6 deletions

View file

@ -90,6 +90,8 @@ Blockly.Flyout = function(workspaceOptions) {
/**
* List of visible buttons.
* @type {!Array.<!Blockly.FlyoutButton>}
* @private
*/
this.buttons_ = [];
@ -196,6 +198,20 @@ Blockly.Flyout.prototype.DRAG_RADIUS = 10;
*/
Blockly.Flyout.prototype.MARGIN = 12;
/**
* Gap between items in horizontal flyouts. Can be overridden with the "sep"
* element.
* @const {number}
*/
Blockly.Flyout.prototype.GAP_X = Blockly.Flyout.prototype.MARGIN * 3;
/**
* Gap between items in vertical flyouts. Can be overridden with the "sep"
* element.
* @const {number}
*/
Blockly.Flyout.prototype.GAP_Y = Blockly.Flyout.prototype.MARGIN * 3;
/**
* Top/bottom padding between scrollbar and edge of flyout background.
* @type {number}
@ -706,6 +722,7 @@ Blockly.Flyout.prototype.show = function(xmlList) {
for (var i = 0, xml; xml = xmlList[i]; i++) {
if (xml.tagName) {
var tagName = xml.tagName.toUpperCase();
var default_gap = this.horizontalLayout_ ? this.GAP_X : this.GAP_Y;
if (tagName == 'BLOCK') {
var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_);
if (curBlock.disabled) {
@ -715,7 +732,7 @@ Blockly.Flyout.prototype.show = function(xmlList) {
}
contents.push({type: 'block', block: curBlock});
var gap = parseInt(xml.getAttribute('gap'), 10);
gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap);
gaps.push(isNaN(gap) ? default_gap : gap);
} else if (xml.tagName.toUpperCase() == 'SEP') {
// Change the gap between two blocks.
// <sep gap="36"></sep>
@ -728,14 +745,14 @@ Blockly.Flyout.prototype.show = function(xmlList) {
if (!isNaN(newGap) && gaps.length > 0) {
gaps[gaps.length - 1] = newGap;
} else {
gaps.push(this.MARGIN * 3);
gaps.push(default_gap);
}
} else if (tagName == 'BUTTON') {
var label = xml.getAttribute('text');
var curButton = new Blockly.FlyoutButton(this.workspace_,
this.targetWorkspace_, label);
contents.push({type: 'button', button: curButton});
gaps.push(this.MARGIN);
gaps.push(default_gap);
}
}
}
@ -806,8 +823,8 @@ Blockly.Flyout.prototype.layout_ = function(contents, gaps) {
this.createCheckbox_(block, cursorX, cursorY, blockHW);
moveX += this.CHECKBOX_SIZE + this.CHECKBOX_MARGIN;
}
block.moveBy(moveX, cursorY);
block.moveBy((this.horizontalLayout_ && this.RTL) ?
cursorX + blockHW.width : cursorX, cursorY);
if (this.horizontalLayout_) {
cursorX += blockHW.width + gaps[i];
} else {
@ -1048,7 +1065,7 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) {
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.Flyout.prototype.onMouseUp_ = function(e) {
Blockly.Flyout.prototype.onMouseUp_ = function(/*e*/) {
if (!this.workspace_.isDragging()) {
if (this.autoClose) {
this.createBlockFunc_(Blockly.Flyout.startBlock_)(

559
core/horizontal_flyout.js Normal file
View file

@ -0,0 +1,559 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2011 Google Inc.
* https://developers.google.com/blockly/
*
* 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 Flyout tray containing blocks which may be created.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.HorizontalFlyout');
goog.require('Blockly.Block');
goog.require('Blockly.Comment');
goog.require('Blockly.Events');
goog.require('Blockly.FlyoutButton');
goog.require('Blockly.Flyout');
goog.require('Blockly.WorkspaceSvg');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.math.Rect');
goog.require('goog.userAgent');
/**
* Class for a flyout.
* @param {!Object} workspaceOptions Dictionary of options for the workspace.
* @extends {Blockly.Flyout}
* @constructor
*/
Blockly.HorizontalFlyout = function(workspaceOptions) {
workspaceOptions.getMetrics = this.getMetrics_.bind(this);
workspaceOptions.setMetrics = this.setMetrics_.bind(this);
Blockly.HorizontalFlyout.superClass_.constructor.call(this, workspaceOptions);
/**
* Flyout should be laid out horizontally vs vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = true;
};
goog.inherits(Blockly.HorizontalFlyout, Blockly.Flyout);
/**
* Return an object with all the metrics required to size scrollbars for the
* flyout. The following properties are computed:
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
* .contentWidth: Width of the contents,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .absoluteTop: Top-edge of view.
* .viewLeft: Offset of the left edge of visible rectangle from parent,
* .contentLeft: Offset of the left-most content from the x=0 coordinate,
* .absoluteLeft: Left-edge of view.
* @return {Object} Contains size and position metrics of the flyout.
* @private
*/
Blockly.HorizontalFlyout.prototype.getMetrics_ = function() {
if (!this.isVisible()) {
// Flyout is hidden.
return null;
}
try {
var optionBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
var optionBox = {height: 0, y: 0, width: 0, x: 0};
}
var absoluteTop = this.SCROLLBAR_PADDING;
var absoluteLeft = this.SCROLLBAR_PADDING;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
absoluteTop = 0;
}
var viewHeight = this.height_;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
viewHeight += this.MARGIN - this.SCROLLBAR_PADDING;
}
var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING;
var metrics = {
viewHeight: viewHeight,
viewWidth: viewWidth,
contentHeight: optionBox.height * this.workspace_.scale + 2 * this.MARGIN,
contentWidth: optionBox.width * this.workspace_.scale + 2 * this.MARGIN,
viewTop: -this.workspace_.scrollY,
viewLeft: -this.workspace_.scrollX,
contentTop: optionBox.y,
contentLeft: optionBox.x,
absoluteTop: absoluteTop,
absoluteLeft: absoluteLeft
};
return metrics;
};
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!Object} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @private
*/
Blockly.HorizontalFlyout.prototype.setMetrics_ = function(xyRatio) {
var metrics = this.getMetrics_();
// This is a fix to an apparent race condition.
if (!metrics) {
return;
}
if (goog.isNumber(xyRatio.x)) {
this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x;
}
this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft,
this.workspace_.scrollY + metrics.absoluteTop);
};
/**
* Move the flyout to the edge of the workspace.
*/
Blockly.HorizontalFlyout.prototype.position = function() {
if (!this.isVisible()) {
return;
}
var targetWorkspaceMetrics = this.targetWorkspace_.getMetrics();
if (!targetWorkspaceMetrics) {
// Hidden components will return null.
return;
}
var edgeWidth = this.horizontalLayout_ ?
targetWorkspaceMetrics.viewWidth : this.width_;
edgeWidth -= this.CORNER_RADIUS;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
edgeWidth *= -1;
}
this.setBackgroundPath_(edgeWidth,
this.horizontalLayout_ ? this.height_ :
targetWorkspaceMetrics.viewHeight);
var x = targetWorkspaceMetrics.absoluteLeft;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
x += targetWorkspaceMetrics.viewWidth;
x -= this.width_;
}
var y = targetWorkspaceMetrics.absoluteTop;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
y += targetWorkspaceMetrics.viewHeight;
y -= this.height_;
}
this.svgGroup_.setAttribute('transform', 'translate(' + x + ',' + y + ')');
// Record the height for Blockly.Flyout.getMetrics_, or width if the layout is
// horizontal.
if (this.horizontalLayout_) {
this.width_ = targetWorkspaceMetrics.viewWidth;
} else {
this.height_ = targetWorkspaceMetrics.viewHeight;
}
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
this.scrollbar_.resize();
}
// The blocks need to be visible in order to be laid out and measured correctly, but we don't
// want the flyout to show up until it's properly sized.
// Opacity is set to zero in show().
this.svgGroup_.style.opacity = 1;
};
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
Blockly.HorizontalFlyout.prototype.setBackgroundPath_ = function(width, height) {
this.setBackgroundPathHorizontal_(width, height);
};
/**
* Create and set the path for the visible boundaries of the flyout in
* horizontal mode.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
*/
Blockly.HorizontalFlyout.prototype.setBackgroundPathHorizontal_ = function(width,
height) {
var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP;
// Start at top left.
var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
// Top.
path.push('h', width + this.CORNER_RADIUS);
// Right.
path.push('v', height);
// Bottom.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('h', -1 * (width - this.CORNER_RADIUS));
// Left.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// Top.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('h', width - this.CORNER_RADIUS);
// Right.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('v', height - this.CORNER_RADIUS);
// Bottom.
path.push('h', -width - this.CORNER_RADIUS);
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
Blockly.HorizontalFlyout.prototype.scrollToStart = function() {
this.scrollbar_.set(this.RTL ? Infinity : 0);
};
/**
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @private
*/
Blockly.HorizontalFlyout.prototype.wheel_ = function(e) {
var delta = e.deltaX;
if (delta) {
if (goog.userAgent.GECKO) {
// Firefox's deltas are a tenth that of Chrome/Safari.
delta *= 10;
}
var metrics = this.getMetrics_();
var pos = metrics.viewLeft + delta;
var limit = metrics.contentWidth - metrics.viewWidth;
pos = Math.min(pos, limit);
pos = Math.max(pos, 0);
this.scrollbar_.set(pos);
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
};
/**
* Lay out the blocks in the flyout.
* @param {!Array.<!Object>} contents The blocks and buttons to lay out.
* @param {!Array.<number>} gaps The visible gaps between blocks.
* @private
*/
Blockly.HorizontalFlyout.prototype.layout_ = function(contents, gaps) {
this.workspace_.scale = this.targetWorkspace_.scale;
var margin = this.MARGIN;
var cursorX = margin;
var cursorY = margin;
if (this.horizontalLayout_ && this.RTL) {
contents = contents.reverse();
}
for (var i = 0, item; item = contents[i]; i++) {
if (item.type == 'block') {
var block = item.block;
var allBlocks = block.getDescendants();
for (var j = 0, child; child = allBlocks[j]; j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such a
// block.
child.isInFlyout = true;
}
var root = block.getSvgRoot();
var blockHW = block.getHeightWidth();
var moveX = cursorX;
if (this.horizontalLayout_ && this.RTL) {
moveX += blockHW.width;
}
if (!this.horizontalLayout_ && block.hasCheckboxInFlyout()) {
this.createCheckbox_(block, cursorX, cursorY, blockHW);
moveX += this.CHECKBOX_SIZE + this.CHECKBOX_MARGIN;
}
block.moveBy((this.horizontalLayout_ && this.RTL) ?
cursorX + blockHW.width : cursorX, cursorY);
if (this.horizontalLayout_) {
cursorX += blockHW.width + gaps[i];
} else {
cursorY += blockHW.height + gaps[i];
}
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
var rect = Blockly.createSvgElement('rect', {'fill-opacity': 0}, null);
rect.tooltip = block;
Blockly.Tooltip.bindMouseEvents(rect);
// Add the rectangles under the blocks, so that the blocks' tooltips work.
this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
block.flyoutRect_ = rect;
this.backgroundButtons_[i] = rect;
this.addBlockListeners_(root, block, rect);
} else if (item.type == 'button') {
var button = item.button;
var buttonSvg = button.createDom();
button.moveTo(cursorX, cursorY);
button.show();
Blockly.bindEvent_(buttonSvg, 'mouseup', button, button.onMouseUp);
this.buttons_.push(button);
if (this.horizontalLayout_) {
cursorX += (button.width + gaps[i]);
} else {
cursorY += button.height + gaps[i];
}
}
}
};
/**
* Handle a mouse-move to drag the flyout.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.HorizontalFlyout.prototype.onMouseMove_ = function(e) {
var metrics = this.getMetrics_();
if (metrics.contentWidth - metrics.viewWidth < 0) {
return;
}
var dx = e.clientX - this.startDragMouseX_;
this.startDragMouseX_ = e.clientX;
var x = metrics.viewLeft - dx;
x = goog.math.clamp(x, 0, metrics.contentWidth - metrics.viewWidth);
this.scrollbar_.set(x);
};
/**
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {number} dx X delta of the drag.
* @param {number} dy Y delta of the drag.
* @return {boolean} true if the drag is toward the workspace.
* @private
*/
Blockly.HorizontalFlyout.prototype.isDragTowardWorkspace_ = function(dx, dy) {
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
var dragDirection = Math.atan2(dy, dx) / Math.PI * 180;
var draggingTowardWorkspace = false;
var range = this.dragAngleRange_;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
// Horizontal at top.
if (dragDirection < 90 + range && dragDirection > 90 - range) {
draggingTowardWorkspace = true;
}
} else {
// Horizontal at bottom.
if (dragDirection > -90 - range && dragDirection < -90 + range) {
draggingTowardWorkspace = true;
}
}
return draggingTowardWorkspace;
};
/**
* Copy a block from the flyout to the workspace and position it correctly.
* @param {!Blockly.Block} originBlock The flyout block to copy..
* @return {!Blockly.Block} The new block in the main workspace.
* @private
*/
Blockly.HorizontalFlyout.prototype.placeNewBlock_ = function(originBlock) {
var targetWorkspace = this.targetWorkspace_;
var svgRootOld = originBlock.getSvgRoot();
if (!svgRootOld) {
throw 'originBlock is not rendered.';
}
// Figure out where the original block is on the screen, relative to the upper
// left corner of the main workspace.
var xyOld = Blockly.getSvgXY_(svgRootOld, targetWorkspace);
// Take into account that the flyout might have been scrolled horizontally
// (separately from the main workspace).
// Generally a no-op in vertical mode but likely to happen in horizontal
// mode.
var scrollX = this.workspace_.scrollX;
var scale = this.workspace_.scale;
xyOld.x += scrollX / scale - scrollX;
// If the flyout is on the right side, (0, 0) in the flyout is offset to
// the right of (0, 0) in the main workspace. Add an offset to take that
// into account.
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
scrollX = targetWorkspace.getMetrics().viewWidth - this.width_;
scale = targetWorkspace.scale;
// Scale the scroll (getSvgXY_ did not do this).
xyOld.x += scrollX / scale - scrollX;
}
// Take into account that the flyout might have been scrolled vertically
// (separately from the main workspace).
// Generally a no-op in horizontal mode but likely to happen in vertical
// mode.
var scrollY = this.workspace_.scrollY;
scale = this.workspace_.scale;
xyOld.y += scrollY / scale - scrollY;
// If the flyout is on the bottom, (0, 0) in the flyout is offset to be below
// (0, 0) in the main workspace. Add an offset to take that into account.
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
scrollY = targetWorkspace.getMetrics().viewHeight - this.height_;
scale = targetWorkspace.scale;
xyOld.y += scrollY / scale - scrollY;
}
// Create the new block by cloning the block in the flyout (via XML).
var xml = Blockly.Xml.blockToDom(originBlock);
var block = Blockly.Xml.domToBlock(xml, targetWorkspace);
var svgRootNew = block.getSvgRoot();
if (!svgRootNew) {
throw 'block is not rendered.';
}
// Figure out where the new block got placed on the screen, relative to the
// upper left corner of the workspace. This may not be the same as the
// original block because the flyout's origin may not be the same as the
// main workspace's origin.
var xyNew = Blockly.getSvgXY_(svgRootNew, targetWorkspace);
// Scale the scroll (getSvgXY_ did not do this).
xyNew.x +=
targetWorkspace.scrollX / targetWorkspace.scale - targetWorkspace.scrollX;
xyNew.y +=
targetWorkspace.scrollY / targetWorkspace.scale - targetWorkspace.scrollY;
// If the flyout is collapsible and the workspace can't be scrolled.
if (targetWorkspace.toolbox_ && !targetWorkspace.scrollbar) {
xyNew.x += targetWorkspace.toolbox_.getWidth() / targetWorkspace.scale;
xyNew.y += targetWorkspace.toolbox_.getHeight() / targetWorkspace.scale;
}
// Move the new block to where the old block is.
block.moveBy(xyOld.x - xyNew.x, xyOld.y - xyNew.y);
return block;
};
/**
* Return the deletion rectangle for this flyout in viewport coordinates.
* @return {goog.math.Rect} Rectangle in which to delete.
*/
Blockly.HorizontalFlyout.prototype.getClientRect = function() {
if (!this.svgGroup_) {
return null;
}
var flyoutRect = this.svgGroup_.getBoundingClientRect();
// BIG_NUM is offscreen padding so that blocks dragged beyond the shown flyout
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
var BIG_NUM = 1000000000;
var y = flyoutRect.top;
var height = flyoutRect.height;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2,
BIG_NUM + height);
} else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2,
BIG_NUM + height);
}
};
/**
* Compute height of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
* @param {!Array<!Blockly.Block>} blocks The blocks to reflow.
*/
Blockly.HorizontalFlyout.prototype.reflowHorizontal = function(blocks) {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutHeight = 0;
for (var i = 0, block; block = blocks[i]; i++) {
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
flyoutHeight += this.MARGIN * 1.5;
flyoutHeight *= this.workspace_.scale;
flyoutHeight += Blockly.Scrollbar.scrollbarThickness;
if (this.height_ != flyoutHeight) {
for (var i = 0, block; block = blocks[i]; i++) {
var blockHW = block.getHeightWidth();
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Rectangles behind blocks with output tabs are shifted a bit.
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('y', blockXY.y);
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width : blockXY.x);
// For hat blocks we want to shift them down by the hat height
// since the y coordinate is the corner, not the top of the hat.
var hatOffset =
block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0;
if (hatOffset) {
block.moveBy(0, hatOffset);
}
block.flyoutRect_.setAttribute('y', blockXY.y);
}
}
// Record the height for .getMetrics_ and .position.
this.height_ = flyoutHeight;
// Call this since it is possible the trash and zoom buttons need
// to move. e.g. on a bottom positioned flyout when zoom is clicked.
this.targetWorkspace_.resize();
}
};
/**
* Reflow blocks and their buttons.
*/
Blockly.HorizontalFlyout.prototype.reflow = function() {
if (this.reflowWrapper_) {
this.workspace_.removeChangeListener(this.reflowWrapper_);
}
var blocks = this.workspace_.getTopBlocks(false);
this.reflowHorizontal(blocks);
if (this.reflowWrapper_) {
this.workspace_.addChangeListener(this.reflowWrapper_);
}
};

1135
core/vertical_flyout.js Normal file

File diff suppressed because it is too large Load diff