Merge pull request #291 from rachel-fenichel/feature/horizontal_toolbox_port

Horizontal toolbox layout with positioning at start or end.
This commit is contained in:
rachel-fenichel
2016-05-13 16:08:30 -07:00
15 changed files with 1195 additions and 223 deletions

View File

@@ -436,6 +436,11 @@ Blockly.hideChaff = function(opt_allowToolbox) {
* .contentLeft: Offset of the left-most content from the x=0 coordinate.
* .absoluteTop: Top-edge of view.
* .absoluteLeft: Left-edge of view.
* .toolboxWidth: Width of toolbox, if it exists. Otherwise zero.
* .toolboxHeight: Height of toolbox, if it exists. Otherwise zero.
* .flyoutWidth: Width of the flyout if it is always open. Otherwise zero.
* .flyoutHeight: Height of flyout if it is always open. Otherwise zero.
* .toolboxPosition: Top, bottom, left or right.
* @return {Object} Contains size and position metrics of main workspace.
* @private
* @this Blockly.WorkspaceSvg
@@ -443,7 +448,13 @@ Blockly.hideChaff = function(opt_allowToolbox) {
Blockly.getMainWorkspaceMetrics_ = function() {
var svgSize = Blockly.svgSize(this.getParentSvg());
if (this.toolbox_) {
svgSize.width -= this.toolbox_.width;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP ||
this.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
svgSize.height -= this.toolbox_.getHeight();
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT ||
this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
svgSize.width -= this.toolbox_.getWidth();
}
}
// Set the margin to match the flyout's margin so that the workspace does
// not jump as blocks are added.
@@ -475,9 +486,14 @@ Blockly.getMainWorkspaceMetrics_ = function() {
var bottomEdge = topEdge + blockBox.height;
}
var absoluteLeft = 0;
if (!this.RTL && this.toolbox_) {
absoluteLeft = this.toolbox_.width;
if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
absoluteLeft = this.toolbox_.getWidth();
}
var absoluteTop = 0;
if (this.toolbox_ && this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
absoluteTop = this.toolbox_.getHeight();
}
var metrics = {
viewHeight: svgSize.height,
viewWidth: svgSize.width,
@@ -487,8 +503,13 @@ Blockly.getMainWorkspaceMetrics_ = function() {
viewLeft: -this.scrollX,
contentTop: topEdge,
contentLeft: leftEdge,
absoluteTop: 0,
absoluteLeft: absoluteLeft
absoluteTop: absoluteTop,
absoluteLeft: absoluteLeft,
toolboxWidth: this.toolbox_ ? this.toolbox_.getWidth() : 0,
toolboxHeight: this.toolbox_ ? this.toolbox_.getHeight() : 0,
flyoutWidth: this.flyout_ ? this.flyout_.getWidth() : 0,
flyoutHeight: this.flyout_ ? this.flyout_.getHeight() : 0,
toolboxPosition: this.toolboxPosition
};
return metrics;
};

View File

@@ -162,3 +162,28 @@ Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE] = Blockly.OUTPUT_VALUE;
Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE] = Blockly.INPUT_VALUE;
Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT] = Blockly.PREVIOUS_STATEMENT;
Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT] = Blockly.NEXT_STATEMENT;
/**
* ENUM for toolbox and flyout at top of screen.
* @const
*/
Blockly.TOOLBOX_AT_TOP = 0;
/**
* ENUM for toolbox and flyout at bottom of screen.
* @const
*/
Blockly.TOOLBOX_AT_BOTTOM = 1;
/**
* ENUM for toolbox and flyout at left of screen.
* @const
*/
Blockly.TOOLBOX_AT_LEFT = 2;
/**
* ENUM for toolbox and flyout at right of screen.
* @const
*/
Blockly.TOOLBOX_AT_RIGHT = 3;

View File

@@ -423,6 +423,16 @@ Blockly.Css.CONTENT = [
'white-space: nowrap;',
'}',
'.blocklyHorizontalTree {',
'float: left;',
'margin: 1px 5px 8px 0;',
'}',
'.blocklyHorizontalTreeRtl {',
'float: right;',
'margin: 1px 0 8px 5px;',
'}',
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {',
'margin-left: 8px;',
'}',
@@ -433,10 +443,18 @@ Blockly.Css.CONTENT = [
'.blocklyTreeSeparator {',
'border-bottom: solid #e5e5e5 1px;',
'height: 0px;',
'height: 0;',
'margin: 5px 0;',
'}',
'.blocklyTreeSeparatorHorizontal {',
'border-right: solid #e5e5e5 1px;',
'width: 0;',
'padding: 5px 0;',
'margin: 0 5px;',
'}',
'.blocklyTreeIcon {',
'background-image: url(<<<PATH>>>/sprites.png);',
'height: 16px;',

View File

@@ -57,6 +57,20 @@ Blockly.Flyout = function(workspaceOptions) {
*/
this.RTL = !!workspaceOptions.RTL;
/**
* Flyout should be laid out horizontally vs vertically.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspaceOptions.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
* @private
*/
this.toolboxPosition_ = workspaceOptions.toolboxPosition;
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {!Array.<!Array>}
@@ -100,6 +114,13 @@ Blockly.Flyout.prototype.autoClose = true;
*/
Blockly.Flyout.prototype.CORNER_RADIUS = 8;
/**
* Margin around the edges of the blocks in the flyout.
* @type {number}
* @const
*/
Blockly.Flyout.prototype.MARGIN = Blockly.Flyout.prototype.CORNER_RADIUS;
/**
* Top/bottom padding between scrollbar and edge of flyout background.
* @type {number}
@@ -149,7 +170,8 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) {
this.targetWorkspace_ = targetWorkspace;
this.workspace_.targetWorkspace = targetWorkspace;
// Add scrollbar.
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, false, false);
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_,
this.horizontalLayout_, false);
this.hide();
@@ -192,15 +214,34 @@ Blockly.Flyout.prototype.dispose = function() {
this.targetWorkspace_ = null;
};
/**
* Get the width of the flyout.
* @return {number} The width of the flyout.
*/
Blockly.Flyout.prototype.getWidth = function() {
return this.width_;
};
/**
* Get the height of the flyout.
* @return {number} The width of the flyout.
*/
Blockly.Flyout.prototype.getHeight = function() {
return this.height_;
};
/**
* 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
@@ -210,46 +251,74 @@ Blockly.Flyout.prototype.getMetrics_ = function() {
// Flyout is hidden.
return null;
}
var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
var viewWidth = this.width_;
try {
var optionBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
var optionBox = {height: 0, y: 0};
var optionBox = {height: 0, y: 0, width: 0, x: 0};
}
return {
var absoluteTop = this.SCROLLBAR_PADDING;
var absoluteLeft = this.SCROLLBAR_PADDING;
if (this.horizontalLayout_) {
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;
} else {
absoluteLeft = 0;
var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
var viewWidth = this.width_;
if (!this.RTL) {
viewWidth -= this.SCROLLBAR_PADDING;
}
}
var metrics = {
viewHeight: viewHeight,
viewWidth: viewWidth,
contentHeight: (optionBox.height + optionBox.y) * this.workspace_.scale,
contentHeight: (optionBox.height + 2 * this.MARGIN) * this.workspace_.scale,
contentWidth: (optionBox.width + 2 * this.MARGIN) * this.workspace_.scale,
viewTop: -this.workspace_.scrollY,
contentTop: 0,
absoluteTop: this.SCROLLBAR_PADDING,
absoluteLeft: 0
viewLeft: -this.workspace_.scrollX,
contentTop: 0, // TODO: #349
contentLeft: 0, // TODO: #349
absoluteTop: absoluteTop,
absoluteLeft: absoluteLeft
};
return metrics;
};
/**
* Sets the Y translation of the flyout to match the scrollbars.
* @param {!Object} yRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling.
* 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.Flyout.prototype.setMetrics_ = function(yRatio) {
Blockly.Flyout.prototype.setMetrics_ = function(xyRatio) {
var metrics = this.getMetrics_();
// This is a fix to an apparent race condition.
if (!metrics) {
return;
}
if (goog.isNumber(yRatio.y)) {
this.workspace_.scrollY =
-metrics.contentHeight * yRatio.y - metrics.contentTop;
if (!this.horizontalLayout_ && goog.isNumber(xyRatio.y)) {
this.workspace_.scrollY = -metrics.contentHeight * xyRatio.y;
} else if (this.horizontalLayout_ && goog.isNumber(xyRatio.x)) {
this.workspace_.scrollX = -metrics.contentWidth * xyRatio.x;
}
this.workspace_.translate(0, this.workspace_.scrollY + metrics.absoluteTop);
this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft,
this.workspace_.scrollY + metrics.absoluteTop);
};
/**
* Move the toolbox to the edge of the workspace.
* Move the flyout to the edge of the workspace.
*/
Blockly.Flyout.prototype.position = function() {
if (!this.isVisible()) {
@@ -260,23 +329,36 @@ Blockly.Flyout.prototype.position = function() {
// Hidden components will return null.
return;
}
var edgeWidth = this.width_ - this.CORNER_RADIUS;
if (this.RTL) {
var edgeWidth = this.horizontalLayout_ ? metrics.viewWidth : this.width_;
edgeWidth -= this.CORNER_RADIUS;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
edgeWidth *= -1;
}
this.setBackgroundPath_(edgeWidth, metrics.viewHeight);
this.setBackgroundPath_(edgeWidth,
this.horizontalLayout_ ? this.height_ : metrics.viewHeight);
var x = metrics.absoluteLeft;
if (this.RTL) {
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
x += metrics.viewWidth;
x -= this.width_;
}
this.svgGroup_.setAttribute('transform',
'translate(' + x + ',' + metrics.absoluteTop + ')');
// Record the height for Blockly.Flyout.getMetrics_.
this.height_ = metrics.viewHeight;
var y = metrics.absoluteTop;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
y += metrics.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_ = metrics.viewWidth;
} else {
this.height_ = metrics.viewHeight;
}
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
@@ -293,57 +375,126 @@ Blockly.Flyout.prototype.position = function() {
* @private
*/
Blockly.Flyout.prototype.setBackgroundPath_ = function(width, height) {
var path = ['M ' + (this.RTL ? this.width_ : 0) + ',0'];
if (this.horizontalLayout_) {
this.setBackgroundPathHorizontal_(width, height);
} else {
this.setBackgroundPathVertical_(width, height);
}
};
/**
* Create and set the path for the visible boundaries of the flyout in vertical
* 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.Flyout.prototype.setBackgroundPathVertical_ = function(width, height) {
var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT;
// Decide whether to start on the left or right.
var path = ['M ' + (atRight ? this.width_ : 0) + ',0'];
// Top.
path.push('h', width);
// Rounded corner.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
this.RTL ? 0 : 1,
this.RTL ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Side closest to the workspace.
// Side closest to workspace.
path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2));
// Rounded corner.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
this.RTL ? 0 : 1,
this.RTL ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Bottom.
path.push('h', -width);
// Side away from the workspace.
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* 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.Flyout.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.Flyout.prototype.scrollToStart = function() {
this.scrollbar_.set(0);
this.scrollbar_.set((this.horizontalLayout_ && this.RTL) ? Infinity : 0);
};
/**
* Scroll the flyout up or down.
* Scroll the flyout.
* @param {!Event} e Mouse wheel scroll event.
* @private
*/
Blockly.Flyout.prototype.wheel_ = function(e) {
var delta = e.deltaY;
var delta = this.horizontalLayout_ ? e.deltaX : e.deltaY;
if (delta) {
if (goog.userAgent.GECKO) {
// Firefox's deltas are a tenth that of Chrome/Safari.
delta *= 10;
}
var metrics = this.getMetrics_();
var y = metrics.viewTop + delta;
y = Math.min(y, metrics.contentHeight - metrics.viewHeight);
y = Math.max(y, 0);
this.scrollbar_.set(y);
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
e.stopPropagation();
var pos = this.horizontalLayout_ ? metrics.viewLeft + delta :
metrics.viewTop + delta;
var limit = this.horizontalLayout_ ?
metrics.contentWidth - metrics.viewWidth :
metrics.contentHeight - metrics.viewHeight;
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();
};
/**
@@ -394,7 +545,6 @@ Blockly.Flyout.prototype.show = function(xmlList) {
Blockly.Procedures.flyoutCategory(this.workspace_.targetWorkspace);
}
var margin = this.CORNER_RADIUS;
this.svgGroup_.style.display = 'block';
// Create the blocks to be shown in this flyout.
var blocks = [];
@@ -402,19 +552,19 @@ Blockly.Flyout.prototype.show = function(xmlList) {
this.permanentlyDisabled_.length = 0;
for (var i = 0, xml; xml = xmlList[i]; i++) {
if (xml.tagName && xml.tagName.toUpperCase() == 'BLOCK') {
var block = Blockly.Xml.domToBlock(xml, this.workspace_);
if (block.disabled) {
var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_);
if (curBlock.disabled) {
// Record blocks that were initially disabled.
// Do not enable these blocks as a result of capacity filtering.
this.permanentlyDisabled_.push(block);
this.permanentlyDisabled_.push(curBlock);
}
blocks.push(block);
blocks.push(curBlock);
var gap = parseInt(xml.getAttribute('gap'), 10);
gaps.push(isNaN(gap) ? margin * 3 : gap);
gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap);
}
}
this.layoutBlocks_(blocks, gaps, margin);
this.layoutBlocks_(blocks, gaps);
// IE 11 is an incompetant browser that fails to fire mouseout events.
// When the mouse is over the background, deselect all blocks.
@@ -427,9 +577,14 @@ Blockly.Flyout.prototype.show = function(xmlList) {
this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
this, deselectAll));
this.width_ = 0;
if (this.horizontalLayout_) {
this.height_ = 0;
} else {
this.width_ = 0;
}
this.reflow();
this.offsetHorizontalRtlBlocks(this.workspace_.getTopBlocks(false));
this.filterForCapacity_();
// Fire a resize event to update the flyout's scrollbar.
@@ -442,11 +597,11 @@ Blockly.Flyout.prototype.show = function(xmlList) {
* Lay out the blocks in the flyout.
* @param {!Array.<!Blockly.BlockSvg>} blocks The blocks to lay out.
* @param {!Array.<number>} gaps The visible gaps between blocks.
* @param {number} margin The margin around the edges of the flyout.
* @private
*/
Blockly.Flyout.prototype.layoutBlocks_ = function(blocks, gaps, margin) {
// Lay out the blocks vertically.
Blockly.Flyout.prototype.layoutBlocks_ = function(blocks, gaps) {
var margin = this.MARGIN * this.workspace_.scale;
var cursorX = this.RTL ? margin : margin + Blockly.BlockSvg.TAB_WIDTH;
var cursorY = margin;
for (var i = 0, block; block = blocks[i]; i++) {
var allBlocks = block.getDescendants();
@@ -459,10 +614,17 @@ Blockly.Flyout.prototype.layoutBlocks_ = function(blocks, gaps, margin) {
block.render();
var root = block.getSvgRoot();
var blockHW = block.getHeightWidth();
var x = this.RTL ? 0 : margin / this.workspace_.scale +
Blockly.BlockSvg.TAB_WIDTH;
block.moveBy(x, cursorY);
cursorY += blockHW.height + gaps[i];
var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
if (this.horizontalLayout_) {
cursorX += tab;
}
block.moveBy((this.horizontalLayout_ && this.RTL) ? -cursorX : cursorX,
cursorY);
if (this.horizontalLayout_) {
cursorX += (blockHW.width + gaps[i] - tab);
} 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.
@@ -484,8 +646,8 @@ Blockly.Flyout.prototype.layoutBlocks_ = function(blocks, gaps, margin) {
*/
Blockly.Flyout.prototype.clearOldBlocks_ = function() {
// Delete any blocks from a previous showing.
var blocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
var oldBlocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = oldBlocks[i]; i++) {
if (block.workspace == this.workspace_) {
block.dispose(false, false);
}
@@ -527,61 +689,6 @@ Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
block.removeSelect));
};
/**
* Compute width of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
*/
Blockly.Flyout.prototype.reflow = function() {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutWidth = 0;
var margin = this.CORNER_RADIUS;
var blocks = this.workspace_.getTopBlocks(false);
for (var x = 0, block; block = blocks[x]; x++) {
var width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= Blockly.BlockSvg.TAB_WIDTH;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
flyoutWidth += Blockly.BlockSvg.TAB_WIDTH;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
if (this.width_ != flyoutWidth) {
for (var x = 0, block; block = blocks[x]; x++) {
var blockHW = block.getHeightWidth();
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
var oldX = block.getRelativeToSurfaceXY().x;
var dx = flyoutWidth - margin;
dx /= this.workspace_.scale;
dx -= Blockly.BlockSvg.TAB_WIDTH;
block.moveBy(dx - oldX, 0);
}
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Blocks with output tabs are shifted a bit.
var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
// 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 width for .getMetrics_ and .position.
this.width_ = flyoutWidth;
// Fire a resize event to update the flyout's scrollbar.
Blockly.asyncSvgResize(this.workspace_);
}
};
/**
* Handle a mouse-down on an SVG block in a non-closing flyout.
* @param {!Blockly.Block} block The flyout block to copy.
@@ -625,6 +732,7 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) {
Blockly.hideChaff(true);
Blockly.Flyout.terminateDrag_();
this.startDragMouseY_ = e.clientY;
this.startDragMouseX_ = e.clientX;
Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove',
this, this.onMouseMove_);
Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup',
@@ -657,13 +765,26 @@ Blockly.Flyout.prototype.onMouseUp_ = function(e) {
* @private
*/
Blockly.Flyout.prototype.onMouseMove_ = function(e) {
var dy = e.clientY - this.startDragMouseY_;
this.startDragMouseY_ = e.clientY;
var metrics = this.getMetrics_();
var y = metrics.viewTop - dy;
y = Math.min(y, metrics.contentHeight - metrics.viewHeight);
y = Math.max(y, 0);
this.scrollbar_.set(y);
if (this.horizontalLayout_) {
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);
} else {
if (metrics.contentHeight - metrics.viewHeight < 0) {
return;
}
var dy = e.clientY - this.startDragMouseY_;
this.startDragMouseY_ = e.clientY;
var y = metrics.viewTop - dy;
y = goog.math.clamp(y, 0, metrics.contentHeight - metrics.viewHeight);
this.scrollbar_.set(y);
}
};
/**
@@ -702,7 +823,6 @@ Blockly.Flyout.prototype.onMouseMoveBlock_ = function(e) {
*/
Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
var flyout = this;
var workspace = this.targetWorkspace_;
return function(e) {
if (Blockly.isRightButton(e)) {
// Right-click. Don't create a block, let the context menu show.
@@ -713,7 +833,7 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
return;
}
Blockly.Events.disable();
var block = flyout.placeNewBlock_(originBlock, workspace);
var block = flyout.placeNewBlock_(originBlock);
Blockly.Events.enable();
if (Blockly.Events.isEnabled()) {
Blockly.Events.setGroup(true);
@@ -733,42 +853,75 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
/**
* Copy a block from the flyout to the workspace and position it correctly.
* @param {!Blockly.Block} originBlock The flyout block to copy.
* @param {!Blockly.Workspace} workspace The main workspace.
* @param {!Blockly.Block} originBlock The flyout block to copy..
* @return {!Blockly.Block} The new block in the main workspace.
* @private
*/
Blockly.Flyout.prototype.placeNewBlock_ = function(originBlock, workspace) {
// 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, workspace);
// Place it in the same spot as the flyout copy.
Blockly.Flyout.prototype.placeNewBlock_ = function(originBlock) {
var targetWorkspace = this.targetWorkspace_;
var svgRootOld = originBlock.getSvgRoot();
if (!svgRootOld) {
throw 'originBlock is not rendered.';
}
var xyOld = Blockly.getSvgXY_(svgRootOld, workspace);
// Scale the scroll (getSvgXY_ did not do this).
if (this.RTL) {
var width = workspace.getMetrics().viewWidth - this.width_;
xyOld.x += width / workspace.scale - width;
} else {
xyOld.x += this.workspace_.scrollX / this.workspace_.scale -
this.workspace_.scrollX;
// 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;
}
xyOld.y += this.workspace_.scrollY / this.workspace_.scale -
this.workspace_.scrollY;
// 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.';
}
var xyNew = Blockly.getSvgXY_(svgRootNew, workspace);
// 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 += workspace.scrollX / workspace.scale - workspace.scrollX;
xyNew.y += workspace.scrollY / workspace.scale - workspace.scrollY;
if (workspace.toolbox_ && !workspace.scrollbar) {
xyNew.x += workspace.toolbox_.width / workspace.scale;
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;
};
@@ -799,13 +952,23 @@ Blockly.Flyout.prototype.getClientRect = function() {
// 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;
if (this.RTL) {
var width = flyoutRect.left + flyoutRect.width + BIG_NUM;
return new goog.math.Rect(flyoutRect.left, -BIG_NUM, width, BIG_NUM * 2);
var x = flyoutRect.left;
var y = flyoutRect.top;
var width = flyoutRect.width;
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);
} else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width,
BIG_NUM * 2);
} else { // Right
return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, BIG_NUM * 2);
}
// LTR
var width = BIG_NUM + flyoutRect.width + flyoutRect.left;
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, width, BIG_NUM * 2);
};
/**
@@ -833,3 +996,141 @@ Blockly.Flyout.terminateDrag_ = function() {
Blockly.Flyout.startBlock_ = null;
Blockly.Flyout.startFlyout_ = null;
};
/**
* 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.Flyout.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 (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 tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('y', blockXY.y);
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
// 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;
Blockly.asyncSvgResize(this.workspace_);
}
};
/**
* Compute width 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.Flyout.prototype.reflowVertical = function(blocks) {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutWidth = 0;
for (var i = 0, block; block = blocks[i]; i++) {
var width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= Blockly.BlockSvg.TAB_WIDTH;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
flyoutWidth += this.MARGIN * 1.5 + Blockly.BlockSvg.TAB_WIDTH;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += Blockly.Scrollbar.scrollbarThickness;
if (this.width_ != flyoutWidth) {
for (var i = 0, block; block = blocks[i]; i++) {
var blockHW = block.getHeightWidth();
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
var oldX = block.getRelativeToSurfaceXY().x;
var dx = flyoutWidth - this.MARGIN;
dx /= this.workspace_.scale;
dx -= Blockly.BlockSvg.TAB_WIDTH;
block.moveBy(dx - oldX, 0);
}
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Blocks with output tabs are shifted a bit.
var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
// 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 width for .getMetrics_ and .position.
this.width_ = flyoutWidth;
Blockly.asyncSvgResize(this.workspace_);
}
};
/**
* Reflow blocks and their buttons.
*/
Blockly.Flyout.prototype.reflow = function() {
var blocks = this.workspace_.getTopBlocks(false);
if (this.horizontalLayout_) {
this.reflowHorizontal(blocks);
} else {
this.reflowVertical(blocks);
}
};
/**
* In the horizontal RTL case all of the blocks will be laid out to the left of
* the origin, but we won't know how big the workspace is until the layout pass
* is done.
* Now that it's done, shunt all the blocks to be right of the origin.
* @param {!Array<!Blockly.Block>} blocks The blocks to reposition.
*/
Blockly.Flyout.prototype.offsetHorizontalRtlBlocks = function(blocks) {
if (this.horizontalLayout_ && this.RTL) {
// We don't know this workspace's view width yet.
this.position();
try {
var optionBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
optionBox = {height: 0, y: 0, width: 0, x: 0};
}
var offset = Math.max(-optionBox.x + this.MARGIN,
this.width_ / this.workspace_.scale);
for (var i = 0, block; block = blocks[i]; i++) {
block.moveBy(offset, 0);
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('x',
offset + Number(block.flyoutRect_.getAttribute('x')));
}
}
}
};

View File

@@ -278,9 +278,10 @@ Blockly.init_ = function(mainWorkspace) {
// Build a fixed flyout with the root blocks.
mainWorkspace.flyout_.init(mainWorkspace);
mainWorkspace.flyout_.show(options.languageTree.childNodes);
mainWorkspace.flyout_.scrollToStart();
// Translate the workspace sideways to avoid the fixed flyout.
mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
if (options.RTL) {
if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
mainWorkspace.scrollX *= -1;
}
mainWorkspace.translate(mainWorkspace.scrollX, 0);

View File

@@ -122,6 +122,9 @@ Blockly.Mutator.prototype.createEditor_ = function() {
parentWorkspace: this.block_.workspace,
pathToMedia: this.block_.workspace.options.pathToMedia,
RTL: this.block_.RTL,
toolboxPosition: this.block_.RTL ? Blockly.TOOLBOX_AT_RIGHT :
Blockly.TOOLBOX_AT_LEFT,
horizontalLayout: false,
getMetrics: this.getFlyoutMetrics_.bind(this),
setMetrics: null
};

View File

@@ -69,6 +69,26 @@ Blockly.Options = function(options) {
hasSounds = true;
}
}
var rtl = !!options['rtl'];
var horizontalLayout = options['horizontalLayout'];
if (horizontalLayout === undefined) {
horizontalLayout = false;
}
var toolboxAtStart = options['toolboxPosition'];
if (toolboxAtStart === 'end') {
toolboxAtStart = false;
} else {
toolboxAtStart = true;
}
if (horizontalLayout) {
var toolboxPosition = toolboxAtStart ?
Blockly.TOOLBOX_AT_TOP : Blockly.TOOLBOX_AT_BOTTOM;
} else {
var toolboxPosition = (toolboxAtStart == rtl) ?
Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT;
}
var hasScrollbars = options['scrollbars'];
if (hasScrollbars === undefined) {
hasScrollbars = hasCategories;
@@ -85,7 +105,7 @@ Blockly.Options = function(options) {
pathToMedia = options['path'] + 'media/';
}
this.RTL = !!options['rtl'];
this.RTL = rtl;
this.collapse = hasCollapse;
this.comments = hasComments;
this.disable = hasDisable;
@@ -97,9 +117,11 @@ Blockly.Options = function(options) {
this.hasTrashcan = hasTrashcan;
this.hasSounds = hasSounds;
this.hasCss = hasCss;
this.horizontalLayout = horizontalLayout;
this.languageTree = languageTree;
this.gridOptions = Blockly.Options.parseGridOptions_(options);
this.zoomOptions = Blockly.Options.parseZoomOptions_(options);
this.toolboxPosition = toolboxPosition;
};
/**

View File

@@ -306,9 +306,9 @@ Blockly.Scrollbar.prototype.resizeHorizontal_ = function(hostMetrics) {
this.svgKnob_.setAttribute('width', Math.max(0, innerLength));
this.xCoordinate = hostMetrics.absoluteLeft + 0.5;
if (this.pair_ && this.workspace_.RTL) {
this.xCoordinate += hostMetrics.absoluteLeft +
Blockly.Scrollbar.scrollbarThickness;
this.xCoordinate += Blockly.Scrollbar.scrollbarThickness;
}
// Horizontal toolbar should always be just above the bottom of the workspace.
this.yCoordinate = hostMetrics.absoluteTop + hostMetrics.viewHeight -
Blockly.Scrollbar.scrollbarThickness - 0.5;
this.svgGroup_.setAttribute('transform',

View File

@@ -31,6 +31,7 @@ goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeStyle');
goog.require('goog.math.Rect');
goog.require('goog.style');
goog.require('goog.ui.tree.TreeControl');
@@ -50,14 +51,80 @@ Blockly.Toolbox = function(workspace) {
* @private
*/
this.workspace_ = workspace;
/**
* Is RTL vs LTR.
* @type {boolean}
*/
this.RTL = workspace.options.RTL;
/**
* Whether the toolbox should be laid out horizontally.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspace.options.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
*/
this.toolboxPosition = workspace.options.toolboxPosition;
/**
* Configuration constants for Closure's tree UI.
* @type {Object.<string,*>}
* @private
*/
this.config_ = {
indentWidth: 19,
cssRoot: 'blocklyTreeRoot',
cssHideRoot: 'blocklyHidden',
cssItem: '',
cssTreeRow: 'blocklyTreeRow',
cssItemLabel: 'blocklyTreeLabel',
cssTreeIcon: 'blocklyTreeIcon',
cssExpandedFolderIcon: 'blocklyTreeIconOpen',
cssFileIcon: 'blocklyTreeIconNone',
cssSelectedRow: 'blocklyTreeSelected'
};
/**
* Configuration constants for tree separator.
* @type {Object.<string,*>}
* @private
*/
this.treeSeparatorConfig_ = {
cssTreeRow: 'blocklyTreeSeparator'
};
if (this.horizontalLayout_) {
this.config_['cssTreeRow'] =
this.config_['cssTreeRow'] +
(workspace.RTL ?
' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
this.treeSeparatorConfig_['cssTreeRow'] =
'blocklyTreeSeparatorHorizontal ' +
(workspace.RTL ?
'blocklyHorizontalTreeRtl' : 'blocklyHorizontalTree');
this.config_['cssTreeIcon'] = '';
}
};
/**
* Width of the toolbox.
* Width of the toolbox, which changes only in vertical layout.
* @type {number}
*/
Blockly.Toolbox.prototype.width = 0;
/**
* Height of the toolbox, which changes only in horizontal layout.
* @type {number}
*/
Blockly.Toolbox.prototype.height = 0;
/**
* The SVG group currently selected.
* @type {SVGGElement}
@@ -72,25 +139,6 @@ Blockly.Toolbox.prototype.selectedOption_ = null;
*/
Blockly.Toolbox.prototype.lastCategory_ = null;
/**
* Configuration constants for Closure's tree UI.
* @type {Object.<string,*>}
* @const
* @private
*/
Blockly.Toolbox.prototype.CONFIG_ = {
indentWidth: 19,
cssRoot: 'blocklyTreeRoot',
cssHideRoot: 'blocklyHidden',
cssItem: '',
cssTreeRow: 'blocklyTreeRow',
cssItemLabel: 'blocklyTreeLabel',
cssTreeIcon: 'blocklyTreeIcon',
cssExpandedFolderIcon: 'blocklyTreeIconOpen',
cssFileIcon: 'blocklyTreeIconNone',
cssSelectedRow: 'blocklyTreeSelected'
};
/**
* Initializes the toolbox.
*/
@@ -116,7 +164,9 @@ Blockly.Toolbox.prototype.init = function() {
var workspaceOptions = {
disabledPatternId: workspace.options.disabledPatternId,
parentWorkspace: workspace,
RTL: workspace.RTL
RTL: workspace.RTL,
horizontalLayout: workspace.horizontalLayout,
toolboxPosition: workspace.options.toolboxPosition
};
/**
* @type {!Blockly.Flyout}
@@ -126,10 +176,10 @@ Blockly.Toolbox.prototype.init = function() {
goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_);
this.flyout_.init(workspace);
this.CONFIG_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
this.CONFIG_['cssCollapsedFolderIcon'] =
this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
this.config_['cssCollapsedFolderIcon'] =
'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr');
var tree = new Blockly.Toolbox.TreeControl(this, this.CONFIG_);
var tree = new Blockly.Toolbox.TreeControl(this, this.config_);
this.tree_ = tree;
tree.setShowRootNode(false);
tree.setShowLines(false);
@@ -152,6 +202,22 @@ Blockly.Toolbox.prototype.dispose = function() {
this.lastCategory_ = null;
};
/**
* Get the width of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.Toolbox.prototype.getWidth = function() {
return this.width;
};
/**
* Get the height of the toolbox.
* @return {number} The width of the toolbox.
*/
Blockly.Toolbox.prototype.getHeight = function() {
return this.height;
};
/**
* Move the toolbox to the edge.
*/
@@ -164,18 +230,31 @@ Blockly.Toolbox.prototype.position = function() {
var svg = this.workspace_.getParentSvg();
var svgPosition = goog.style.getPageOffset(svg);
var svgSize = Blockly.svgSize(svg);
if (this.workspace_.RTL) {
treeDiv.style.left =
(svgPosition.x + svgSize.width - treeDiv.offsetWidth) + 'px';
} else {
if (this.horizontalLayout_) {
treeDiv.style.left = svgPosition.x + 'px';
}
treeDiv.style.height = svgSize.height + 'px';
treeDiv.style.top = svgPosition.y + 'px';
this.width = treeDiv.offsetWidth;
if (!this.workspace_.RTL) {
// For some reason the LTR toolbox now reports as 1px too wide.
this.width -= 1;
treeDiv.style.height = 'auto';
treeDiv.style.width = svgSize.width + 'px';
this.height = treeDiv.offsetHeight;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
treeDiv.style.top = svgPosition.y + 'px';
} else { // Bottom
var topOfToolbox = svgPosition.y + svgSize.height - treeDiv.offsetHeight;
treeDiv.style.top = topOfToolbox + 'px';
}
} else {
if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
treeDiv.style.left =
(svgPosition.x + svgSize.width - treeDiv.offsetWidth) + 'px';
} else { // Left
treeDiv.style.left = svgPosition.x + 'px';
}
treeDiv.style.height = svgSize.height + 'px';
treeDiv.style.top = svgPosition.y + 'px';
this.width = treeDiv.offsetWidth;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
// For some reason the LTR toolbox now reports as 1px too wide.
this.width -= 1;
}
}
this.flyout_.position();
};
@@ -187,10 +266,11 @@ Blockly.Toolbox.prototype.position = function() {
*/
Blockly.Toolbox.prototype.populate_ = function(newTree) {
var rootOut = this.tree_;
var that = this;
rootOut.removeChildren(); // Delete any existing content.
rootOut.blocks = [];
var hasColours = false;
function syncTrees(treeIn, treeOut) {
function syncTrees(treeIn, treeOut, pathToMedia) {
var lastElement = null;
for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) {
if (!childIn.tagName) {
@@ -207,7 +287,7 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
// Variables and procedures are special dynamic categories.
childOut.blocks = custom;
} else {
syncTrees(childIn, childOut);
syncTrees(childIn, childOut, pathToMedia);
}
var colour = childIn.getAttribute('colour');
if (goog.isString(colour)) {
@@ -235,7 +315,8 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
if (lastElement.tagName.toUpperCase() == 'CATEGORY') {
// Separator between two categories.
// <sep></sep>
treeOut.add(new Blockly.Toolbox.TreeSeparator());
treeOut.add(new Blockly.Toolbox.TreeSeparator(
that.treeSeparatorConfig_));
} else {
// Change the gap between two blocks.
// <sep gap="36"></sep>
@@ -259,7 +340,7 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
}
}
}
syncTrees(newTree, this.tree_);
syncTrees(newTree, this.tree_, this.workspace_.options.pathToMedia);
this.hasColours_ = hasColours;
if (rootOut.blocks.length) {
@@ -313,16 +394,26 @@ Blockly.Toolbox.prototype.getClientRect = function() {
// area are still deleted. Must be smaller than Infinity, but larger than
// the largest screen size.
var BIG_NUM = 10000000;
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
var x = toolboxRect.left;
var y = toolboxRect.top;
var width = toolboxRect.width;
var height = toolboxRect.height;
// Assumes that the toolbox is on the SVG edge. If this changes
// (e.g. toolboxes in mutators) then this code will need to be more complex.
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
if (this.workspace_.RTL) {
var width = toolboxRect.left + toolboxRect.width + BIG_NUM;
return new goog.math.Rect(toolboxRect.left, -BIG_NUM, width, BIG_NUM * 2);
if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + x + width,
2 * BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM,
BIG_NUM + y + height);
} else { // Bottom
return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM + width);
}
// LTR
var width = BIG_NUM + toolboxRect.width + toolboxRect.left;
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, width, BIG_NUM * 2);
};
// Extending Closure's Tree UI.
@@ -495,18 +586,7 @@ Blockly.Toolbox.TreeNode.prototype.onDoubleClick_ = function(e) {
* @constructor
* @extends {Blockly.Toolbox.TreeNode}
*/
Blockly.Toolbox.TreeSeparator = function() {
Blockly.Toolbox.TreeNode.call(this, null, '',
Blockly.Toolbox.TreeSeparator.CONFIG_);
Blockly.Toolbox.TreeSeparator = function(config) {
Blockly.Toolbox.TreeNode.call(this, null, '', config);
};
goog.inherits(Blockly.Toolbox.TreeSeparator, Blockly.Toolbox.TreeNode);
/**
* Configuration constants for tree separator.
* @type {Object.<string,*>}
* @const
* @private
*/
Blockly.Toolbox.TreeSeparator.CONFIG_ = {
cssTreeRow: 'blocklyTreeSeparator'
};

View File

@@ -220,12 +220,26 @@ Blockly.Trashcan.prototype.position = function() {
}
if (this.workspace_.RTL) {
this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
this.left_ += metrics.flyoutWidth;
if (this.workspace_.toolbox_) {
this.left_ += metrics.absoluteLeft;
}
}
} else {
this.left_ = metrics.viewWidth + metrics.absoluteLeft -
this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
this.left_ -= metrics.flyoutWidth;
}
}
this.top_ = metrics.viewHeight + metrics.absoluteTop -
(this.BODY_HEIGHT_ + this.LID_HEIGHT_) - this.bottom_;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ -= metrics.flyoutHeight;
}
this.svgGroup_.setAttribute('transform',
'translate(' + this.left_ + ',' + this.top_ + ')');
};

View File

@@ -43,6 +43,11 @@ Blockly.Workspace = function(opt_options) {
this.options = opt_options || {};
/** @type {boolean} */
this.RTL = !!this.options.RTL;
/** @type {boolean} */
this.horizontalLayout = !!this.options.horizontalLayout;
/** @type {number} */
this.toolboxPosition = this.options.toolboxPosition;
/**
* @type {!Array.<!Blockly.Block>}
* @private

View File

@@ -281,7 +281,9 @@ Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
var workspaceOptions = {
disabledPatternId: this.options.disabledPatternId,
parentWorkspace: this,
RTL: this.RTL
RTL: this.RTL,
horizontalLayout: this.horizontalLayout,
toolboxPosition: this.options.toolboxPosition,
};
/** @type {Blockly.Flyout} */
this.flyout_ = new Blockly.Flyout(workspaceOptions);

View File

@@ -215,12 +215,25 @@ Blockly.ZoomControls.prototype.position = function() {
}
if (this.workspace_.RTL) {
this.left_ = this.MARGIN_SIDE_ + Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
this.left_ += metrics.flyoutWidth;
if (this.workspace_.toolbox_) {
this.left_ += metrics.absoluteLeft;
}
}
} else {
this.left_ = metrics.viewWidth + metrics.absoluteLeft -
this.WIDTH_ - this.MARGIN_SIDE_ - Blockly.Scrollbar.scrollbarThickness;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
this.left_ -= metrics.flyoutWidth;
}
}
this.top_ = metrics.viewHeight + metrics.absoluteTop -
this.HEIGHT_ - this.bottom_;
if (metrics.toolboxPosition == Blockly.TOOLBOX_AT_BOTTOM) {
this.top_ -= metrics.flyoutHeight;
}
this.svgGroup_.setAttribute('transform',
'translate(' + this.left_ + ',' + this.top_ + ')');
};

View File

@@ -294,7 +294,7 @@ Code.tabClick = function(clickedName) {
if (clickedName == 'blocks') {
Code.workspace.setVisible(true);
}
Blockly.fireUiEvent(window, 'resize');
Blockly.asyncSvgResize(this.workspace_);
};
/**

467
tests/multi_playground.html Normal file
View File

@@ -0,0 +1,467 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Multi-toolbox Playground</title>
<script src="../blockly_uncompressed.js"></script>
<script src="../msg/messages.js"></script>
<script src="../blocks/logic.js"></script>
<script src="../blocks/loops.js"></script>
<script src="../blocks/math.js"></script>
<script src="../blocks/text.js"></script>
<script src="../blocks/lists.js"></script>
<script src="../blocks/colour.js"></script>
<script src="../blocks/variables.js"></script>
<script src="../blocks/procedures.js"></script>
<script>
'use strict';
function start() {
startBlocklyInstance('VertStartLTR', false, false, 'start');
startBlocklyInstance('VertStartRTL', true, false, 'start');
startBlocklyInstance('VertEndLTR', false, false, 'end');
startBlocklyInstance('VertEndRTL', true, false, 'end');
startBlocklyInstance('HorizontalStartLTR', false, true, 'start');
startBlocklyInstance('HorizontalStartRTL', true, true, 'start');
startBlocklyInstance('HorizontalEndLTR', false, true, 'end');
startBlocklyInstance('HorizontalEndRTL', true, true, 'end');
}
function startBlocklyInstance(suffix, rtl, horizontalLayout, position) {
var toolbox = document.getElementById('toolbox_categoriesScroll');
var options = {
comments: false,
disable: false,
collapse: false,
maxBlocks: Infinity,
media: '../media/',
readOnly: false,
rtl: rtl,
scrollbars: true,
toolbox: toolbox,
trashcan: true,
horizontalLayout: horizontalLayout,
toolboxPosition: position,
zoom: {
controls: true,
wheel: false,
startScale: 1.0,
maxScale: 4,
minScale: 0.25,
scaleSpeed: 1.1
},
};
Blockly.inject('blocklyDiv' + suffix, options);
}
</script>
<style>
html, body {
height: 100%;
}
body {
background-color: #fff;
font-family: sans-serif;
}
h1 {
font-weight: normal;
font-size: 140%;
}
#blocklyDiv {
float: right;
height: 95%;
width: 70%;
}
#collaborators {
float: right;
width: 30px;
margin-left: 10px;
}
#collaborators > img {
margin-right: 5px;
height: 30px;
padding-bottom: 5px;
width: 30px;
border-radius: 3px;
}
#importExport {
font-family: monospace;
}
</style>
</head>
<body onload="start()">
<div id="collaborators"></div>
<table>
<tr>
<td/>
<td>LTR</td>
<td>RTL</td>
</tr>
<tr>
<td>Vertical layout; toolbox at start</td>
<td>
<div id="blocklyDivVertStartLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivVertStartRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
<tr>
<td>Vertical layout; toolbox at end</td>
<td>
<div id="blocklyDivVertEndLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivVertEndRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
<tr>
<td>Horizontal layout; toolbox at start</td>
<td>
<div id="blocklyDivHorizontalStartLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivHorizontalStartRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
<tr>
<td>Horizontal layout; toolbox at end</td>
<td>
<div id="blocklyDivHorizontalEndLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivHorizontalEndRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
</table>
<xml id="toolbox_alwaysOpen" style="display: none">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<!-- <block type="control_repeat"></block> -->
<block type="logic_operation"></block>
<block type="controls_repeat_ext">
<value name="TIMES">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="logic_operation"></block>
<block type="logic_negate"></block>
<block type="logic_boolean"></block>
<block type="logic_null" disabled="true"></block>
<block type="logic_ternary"></block>
</xml>
<xml id="toolbox_categoriesScroll" style="display: none">
<category name="Logic" colour="210">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_negate"></block>
<block type="logic_boolean"></block>
<block type="logic_null" disabled="true"></block>
<block type="logic_ternary"></block>
</category>
<category name="Loops" colour="120">
<block type="controls_repeat_ext">
<value name="TIMES">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="controls_repeat" disabled="true"></block>
<block type="controls_whileUntil"></block>
<block type="controls_for">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
<value name="BY">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="controls_forEach"></block>
<block type="controls_flow_statements"></block>
</category>
<category name="Math" colour="230">
<block type="math_number" gap="32"></block>
<block type="math_arithmetic">
<value name="A">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="B">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_single">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">9</field>
</shadow>
</value>
</block>
<block type="math_trig">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">45</field>
</shadow>
</value>
</block>
<block type="math_constant"></block>
<block type="math_number_property">
<value name="NUMBER_TO_CHECK">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="math_change">
<value name="DELTA">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_round">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">3.1</field>
</shadow>
</value>
</block>
<block type="math_on_list"></block>
<block type="math_modulo">
<value name="DIVIDEND">
<shadow type="math_number">
<field name="NUM">64</field>
</shadow>
</value>
<value name="DIVISOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="math_constrain">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="LOW">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="HIGH">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_int">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_float"></block>
</category>
<category name="Text" colour="160">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_append">
<value name="TEXT">
<shadow type="text"></shadow>
</value>
</block>
<block type="text_length">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_isEmpty">
<value name="VALUE">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="text_indexOf">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
<value name="FIND">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_charAt">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
</block>
<block type="text_getSubstring">
<value name="STRING">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
</block>
<block type="text_changeCase">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_trim">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_print">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_prompt_ext">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
</category>
<category name="Lists" colour="260">
<block type="lists_create_with">
<mutation items="0"></mutation>
</block>
<block type="lists_create_with"></block>
<block type="lists_repeat">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
</block>
<block type="lists_length"></block>
<block type="lists_isEmpty"></block>
<block type="lists_indexOf">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_getIndex">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_setIndex">
<value name="LIST">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_getSublist">
<value name="LIST">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_split">
<value name="DELIM">
<shadow type="text">
<field name="TEXT">,</field>
</shadow>
</value>
</block>
</category>
<category name="Colour" colour="20">
<block type="colour_picker"></block>
<block type="colour_random"></block>
<block type="colour_rgb">
<value name="RED">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
<value name="GREEN">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="BLUE">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="colour_blend">
<value name="COLOUR1">
<shadow type="colour_picker">
<field name="COLOUR">#ff0000</field>
</shadow>
</value>
<value name="COLOUR2">
<shadow type="colour_picker">
<field name="COLOUR">#3333ff</field>
</shadow>
</value>
<value name="RATIO">
<shadow type="math_number">
<field name="NUM">0.5</field>
</shadow>
</value>
</block>
</category>
<sep></sep>
<category name="Variables" colour="330" custom="VARIABLE"></category>
<category name="Functions" colour="290" custom="PROCEDURE"></category>
</xml>
</body>
</html>