mirror of
https://github.com/google/blockly.git
synced 2026-01-10 02:17:09 +01:00
Added flyout to trashcan to "get back" deleted blocks.`
This commit is contained in:
@@ -326,8 +326,14 @@ Blockly.onContextMenu_ = function(e) {
|
||||
Blockly.hideChaff = function(opt_allowToolbox) {
|
||||
Blockly.Tooltip.hide();
|
||||
Blockly.WidgetDiv.hide();
|
||||
// For now the trashcan flyout always autocloses because it overlays the
|
||||
// trashcan UI (no trashcan to click to close it)
|
||||
var workspace = Blockly.getMainWorkspace();
|
||||
if (workspace.trashcan &&
|
||||
workspace.trashcan.flyout_) {
|
||||
workspace.trashcan.flyout_.hide();
|
||||
}
|
||||
if (!opt_allowToolbox) {
|
||||
var workspace = Blockly.getMainWorkspace();
|
||||
if (workspace.toolbox_ &&
|
||||
workspace.toolbox_.flyout_ &&
|
||||
workspace.toolbox_.flyout_.autoClose) {
|
||||
|
||||
@@ -218,6 +218,12 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface,
|
||||
var flyout = mainWorkspace.addFlyout_('svg');
|
||||
Blockly.utils.insertAfter(flyout, svg);
|
||||
}
|
||||
if (options.hasTrashcan) {
|
||||
mainWorkspace.addTrashcan_();
|
||||
}
|
||||
if (options.zoomOptions && options.zoomOptions.controls) {
|
||||
mainWorkspace.addZoomControls_();
|
||||
}
|
||||
|
||||
// A null translation will also apply the correct initial scale.
|
||||
mainWorkspace.translate(0, 0);
|
||||
@@ -322,6 +328,14 @@ Blockly.init_ = function(mainWorkspace) {
|
||||
}
|
||||
}
|
||||
|
||||
var bottom = Blockly.Scrollbar.scrollbarThickness;
|
||||
if (options.hasTrashcan) {
|
||||
bottom = mainWorkspace.trashcan.init(bottom);
|
||||
}
|
||||
if (options.zoomOptions && options.zoomOptions.controls) {
|
||||
mainWorkspace.zoomControls_.init(bottom);
|
||||
}
|
||||
|
||||
if (options.hasScrollbars) {
|
||||
mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
|
||||
mainWorkspace.scrollbar.resize();
|
||||
|
||||
159
core/trashcan.js
159
core/trashcan.js
@@ -38,6 +38,19 @@ goog.require('goog.math.Rect');
|
||||
*/
|
||||
Blockly.Trashcan = function(workspace) {
|
||||
this.workspace_ = workspace;
|
||||
var flyoutWorkspaceOptions = {
|
||||
scrollbars: true,
|
||||
disabledPatternId: this.workspace_.options.disabledPatternId,
|
||||
parentWorkspace: this.workspace_,
|
||||
RTL: this.workspace_.RTL,
|
||||
oneBasedIndex: workspace.options.oneBasedIndex,
|
||||
// TODO: Add horizontal.
|
||||
/*horizontalLayout: this.workspace_.horizontalLayout,*/
|
||||
toolboxPosition: this.workspace_.RTL ? Blockly.TOOLBOX_AT_LEFT :
|
||||
Blockly.TOOLBOX_AT_RIGHT
|
||||
};
|
||||
this.flyout_ = new Blockly.VerticalFlyout(flyoutWorkspaceOptions);
|
||||
this.workspace_.addChangeListener(this.onDelete_());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,12 +109,46 @@ Blockly.Trashcan.prototype.SPRITE_LEFT_ = 0;
|
||||
*/
|
||||
Blockly.Trashcan.prototype.SPRITE_TOP_ = 32;
|
||||
|
||||
/**
|
||||
* The openness of the lid when the trashcan contains blocks.
|
||||
* (0.0 = closed, 1.0 = open)
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.HAS_BLOCKS_LID_ANGLE = 0.1;
|
||||
|
||||
/**
|
||||
* The maximum number of blocks that can go in the trashcan's flyout. '0' turns
|
||||
* turns off trashcan contents, 'Infinity' sets it to unlimited.
|
||||
* @type {number}
|
||||
*/
|
||||
Blockly.Trashcan.prototype.MAX_CONTENTS = 32;
|
||||
|
||||
/**
|
||||
* Current open/close state of the lid.
|
||||
* @type {boolean}
|
||||
*/
|
||||
Blockly.Trashcan.prototype.isOpen = false;
|
||||
|
||||
/**
|
||||
* The minimum openness of the lid. Used to indicate if the trashcan contains
|
||||
* blocks.
|
||||
* @type {number}
|
||||
*/
|
||||
Blockly.Trashcan.prototype.minOpenness_ = 0;
|
||||
|
||||
/**
|
||||
* True if the trashcan contains blocks, otherwise false.
|
||||
* @type {boolean}
|
||||
*/
|
||||
Blockly.Trashcan.prototype.hasBlocks = false;
|
||||
|
||||
/**
|
||||
* A list of Xml representing blocks "inside" the trashcan.
|
||||
* @type {Array}
|
||||
*/
|
||||
Blockly.Trashcan.prototype.contents = [];
|
||||
|
||||
/**
|
||||
* The SVG group containing the trash can.
|
||||
* @type {Element}
|
||||
@@ -207,6 +254,11 @@ Blockly.Trashcan.prototype.createDom = function() {
|
||||
this.workspace_.options.pathToMedia + Blockly.SPRITE.url);
|
||||
|
||||
Blockly.bindEventWithChecks_(this.svgGroup_, 'mouseup', this, this.click);
|
||||
// bindEventWithChecks_ quashes events too aggressively. See:
|
||||
// https://groups.google.com/forum/#!topic/blockly/QF4yB9Wx00s
|
||||
// Bind to body instead of this.svgGroup_ so that we don't get lid jitters
|
||||
Blockly.bindEvent_(body, 'mouseover', this, this.mouseOver_);
|
||||
Blockly.bindEvent_(body, 'mouseout', this, this.mouseOut_);
|
||||
this.animateLid_();
|
||||
return this.svgGroup_;
|
||||
};
|
||||
@@ -217,6 +269,10 @@ Blockly.Trashcan.prototype.createDom = function() {
|
||||
* @return {number} Distance from workspace bottom to the top of trashcan.
|
||||
*/
|
||||
Blockly.Trashcan.prototype.init = function(bottom) {
|
||||
Blockly.utils.insertAfter(this.flyout_.createDom('svg'),
|
||||
this.workspace_.getParentSvg());
|
||||
this.flyout_.init(this.workspace_);
|
||||
|
||||
this.bottom_ = this.MARGIN_BOTTOM_ + bottom;
|
||||
this.setOpen_(false);
|
||||
return this.bottom_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_;
|
||||
@@ -240,6 +296,9 @@ Blockly.Trashcan.prototype.dispose = function() {
|
||||
* Move the trash can to the bottom-right corner.
|
||||
*/
|
||||
Blockly.Trashcan.prototype.position = function() {
|
||||
if (!this.bottom_) {
|
||||
return;
|
||||
}
|
||||
var metrics = this.workspace_.getMetrics();
|
||||
if (!metrics) {
|
||||
// There are no metrics available (workspace is probably not visible).
|
||||
@@ -309,20 +368,28 @@ Blockly.Trashcan.prototype.setOpen_ = function(state) {
|
||||
*/
|
||||
Blockly.Trashcan.prototype.animateLid_ = function() {
|
||||
this.lidOpen_ += this.isOpen ? 0.2 : -0.2;
|
||||
this.lidOpen_ = Math.min(Math.max(this.lidOpen_, 0), 1);
|
||||
var lidAngle = this.lidOpen_ * 45;
|
||||
this.svgLid_.setAttribute('transform', 'rotate(' +
|
||||
(this.workspace_.RTL ? -lidAngle : lidAngle) + ',' +
|
||||
(this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' +
|
||||
(this.LID_HEIGHT_ - 2) + ')');
|
||||
this.lidOpen_ = Math.min(Math.max(this.lidOpen_, this.minOpenness_), 1);
|
||||
this.setLidAngle_(this.lidOpen_ * 45);
|
||||
// Linear interpolation between 0.4 and 0.8.
|
||||
var opacity = 0.4 + this.lidOpen_ * (0.8 - 0.4);
|
||||
this.svgGroup_.style.opacity = opacity;
|
||||
if (this.lidOpen_ > 0 && this.lidOpen_ < 1) {
|
||||
if (this.lidOpen_ > this.minOpenness_ && this.lidOpen_ < 1) {
|
||||
this.lidTask_ = setTimeout(this.animateLid_.bind(this), 20);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the angle of the trashcan's lid.
|
||||
* @param {!number} lidAngle The angle at which to set the lid.
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.setLidAngle_ = function(lidAngle) {
|
||||
this.svgLid_.setAttribute('transform', 'rotate(' +
|
||||
(this.workspace_.RTL ? -lidAngle : lidAngle) + ',' +
|
||||
(this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' +
|
||||
(this.LID_HEIGHT_ - 2) + ')');
|
||||
};
|
||||
|
||||
/**
|
||||
* Flip the lid shut.
|
||||
* Called externally after a drag.
|
||||
@@ -335,10 +402,80 @@ Blockly.Trashcan.prototype.close = function() {
|
||||
* Inspect the contents of the trash.
|
||||
*/
|
||||
Blockly.Trashcan.prototype.click = function() {
|
||||
var dx = this.workspace_.startScrollX - this.workspace_.scrollX;
|
||||
var dy = this.workspace_.startScrollY - this.workspace_.scrollY;
|
||||
if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
|
||||
if (!this.hasBlocks) {
|
||||
return;
|
||||
}
|
||||
console.log('TODO: Inspect trash.');
|
||||
|
||||
var xml = [];
|
||||
for (var i = 0, text; text = this.contents[i]; i++) {
|
||||
xml[i] = Blockly.Xml.textToDom(text).firstChild;
|
||||
}
|
||||
this.flyout_.show(xml);
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicate that the trashcan can be clicked (by opening it) if it has blocks.
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.mouseOver_ = function() {
|
||||
if (!this.hasBlocks) {
|
||||
return;
|
||||
}
|
||||
this.setOpen_(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the lid of the trashcan if it was open (Vis. it was indicating it had
|
||||
* blocks).
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.mouseOut_ = function() {
|
||||
// No need to do a .hasBlocks check here because if it doesn't the trashcan
|
||||
// wont be open in the first place, and setOpen_ won't run.
|
||||
this.setOpen_(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a BLOCK_DELETE event. Adds deleted blocks oldXml to the content array.
|
||||
* @returns {!Function} Function to call when a block is deleted.
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.onDelete_ = function() {
|
||||
var trashcan = this;
|
||||
return function(event) {
|
||||
if (!trashcan.MAX_CONTENTS) {
|
||||
return;
|
||||
}
|
||||
if (event.type == Blockly.Events.BLOCK_DELETE) {
|
||||
var textBlock = trashcan.blockXmlToText(event.oldXml);
|
||||
if (trashcan.contents.indexOf(textBlock) != -1) {
|
||||
return;
|
||||
}
|
||||
trashcan.contents.unshift(textBlock);
|
||||
if (trashcan.contents.length > trashcan.MAX_CONTENTS) {
|
||||
trashcan.contents.splice(trashcan.MAX_CONTENTS,
|
||||
trashcan.contents.length - trashcan.MAX_CONTENTS);
|
||||
}
|
||||
|
||||
trashcan.hasBlocks = true;
|
||||
trashcan.minOpenness_ = trashcan.HAS_BLOCKS_LID_ANGLE;
|
||||
trashcan.setLidAngle_(trashcan.minOpenness_);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts xml representing a block into text that can be stored in the
|
||||
* content array.
|
||||
* @param {!Element} xml An XML tree defining the block and any
|
||||
* connected child blocks.
|
||||
* @returns {string} The XML tree converted into a usable text string.
|
||||
*/
|
||||
Blockly.Trashcan.prototype.blockXmlToText = function(xml) {
|
||||
var xmlBlock = xml.cloneNode(true);
|
||||
Blockly.Xml.deleteNext(xmlBlock);
|
||||
xmlBlock.removeAttribute('x');
|
||||
xmlBlock.removeAttribute('y');
|
||||
xmlBlock.removeAttribute('id');
|
||||
return '<xml>' + Blockly.Xml.domToText(xmlBlock) + '</xml>';
|
||||
};
|
||||
|
||||
@@ -466,13 +466,6 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
|
||||
/** @type {SVGElement} */
|
||||
this.svgBubbleCanvas_ = Blockly.utils.createSvgElement('g',
|
||||
{'class': 'blocklyBubbleCanvas'}, this.svgGroup_);
|
||||
var bottom = Blockly.Scrollbar.scrollbarThickness;
|
||||
if (this.options.hasTrashcan) {
|
||||
bottom = this.addTrashcan_(bottom);
|
||||
}
|
||||
if (this.options.zoomOptions && this.options.zoomOptions.controls) {
|
||||
this.addZoomControls_(bottom);
|
||||
}
|
||||
|
||||
if (!this.isFlyout) {
|
||||
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
|
||||
@@ -582,30 +575,24 @@ Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
|
||||
|
||||
/**
|
||||
* Add a trashcan.
|
||||
* @param {number} bottom Distance from workspace bottom to bottom of trashcan.
|
||||
* @return {number} Distance from workspace bottom to the top of trashcan.
|
||||
* @private
|
||||
*/
|
||||
Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
|
||||
Blockly.WorkspaceSvg.prototype.addTrashcan_ = function() {
|
||||
/** @type {Blockly.Trashcan} */
|
||||
this.trashcan = new Blockly.Trashcan(this);
|
||||
var svgTrashcan = this.trashcan.createDom();
|
||||
this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
|
||||
return this.trashcan.init(bottom);
|
||||
this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_).nextSibling;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add zoom controls.
|
||||
* @param {number} bottom Distance from workspace bottom to bottom of controls.
|
||||
* @return {number} Distance from workspace bottom to the top of controls.
|
||||
* @private
|
||||
*/
|
||||
Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
|
||||
Blockly.WorkspaceSvg.prototype.addZoomControls_ = function() {
|
||||
/** @type {Blockly.ZoomControls} */
|
||||
this.zoomControls_ = new Blockly.ZoomControls(this);
|
||||
var svgZoomControls = this.zoomControls_.createDom();
|
||||
this.svgGroup_.appendChild(svgZoomControls);
|
||||
return this.zoomControls_.init(bottom);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -132,6 +132,9 @@ Blockly.ZoomControls.prototype.dispose = function() {
|
||||
* Move the zoom controls to the bottom-right corner.
|
||||
*/
|
||||
Blockly.ZoomControls.prototype.position = function() {
|
||||
if (!this.bottom_) {
|
||||
return;
|
||||
}
|
||||
var metrics = this.workspace_.getMetrics();
|
||||
if (!metrics) {
|
||||
// There are no metrics available (workspace is probably not visible).
|
||||
|
||||
Reference in New Issue
Block a user