mirror of
https://github.com/google/blockly.git
synced 2026-01-09 01:50:11 +01:00
Merge pull request #2157 from BeksOmega/feature/TrashcanContents
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);
|
||||
@@ -339,6 +345,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();
|
||||
|
||||
191
core/trashcan.js
191
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,50 @@ 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}
|
||||
* @private
|
||||
*/
|
||||
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}
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.minOpenness_ = 0;
|
||||
|
||||
/**
|
||||
* True if the trashcan contains blocks, otherwise false.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.hasBlocks_ = false;
|
||||
|
||||
/**
|
||||
* A list of Xml (stored as strings) representing blocks "inside" the trashcan.
|
||||
* @type {Array}
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.contents_ = [];
|
||||
|
||||
/**
|
||||
* The SVG group containing the trash can.
|
||||
* @type {Element}
|
||||
@@ -207,6 +258,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 +273,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 +300,10 @@ Blockly.Trashcan.prototype.dispose = function() {
|
||||
* Move the trash can to the bottom-right corner.
|
||||
*/
|
||||
Blockly.Trashcan.prototype.position = function() {
|
||||
// Not yet initialized.
|
||||
if (!this.bottom_) {
|
||||
return;
|
||||
}
|
||||
var metrics = this.workspace_.getMetrics();
|
||||
if (!metrics) {
|
||||
// There are no metrics available (workspace is probably not visible).
|
||||
@@ -309,20 +373,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 +407,107 @@ 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 cleanedXML = trashcan.cleanBlockXML_(event.oldXml);
|
||||
if (trashcan.contents_.indexOf(cleanedXML) != -1) {
|
||||
return;
|
||||
}
|
||||
trashcan.contents_.unshift(cleanedXML);
|
||||
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_ * 45);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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} Text representing the XML tree, cleaned of all unnecessary
|
||||
* attributes.
|
||||
* @private
|
||||
*/
|
||||
Blockly.Trashcan.prototype.cleanBlockXML_ = function(xml) {
|
||||
var xmlBlock = xml.cloneNode(true);
|
||||
var node = xmlBlock;
|
||||
while (node) {
|
||||
// Things like text inside tags are still treated as nodes, but they
|
||||
// don't have attributes (or the removeAttribute function) so we can
|
||||
// skip removing attributes from them.
|
||||
if (node.removeAttribute) {
|
||||
node.removeAttribute('x');
|
||||
node.removeAttribute('y');
|
||||
node.removeAttribute('id');
|
||||
}
|
||||
|
||||
// Try to go down the tree
|
||||
var nextNode = node.firstChild || node.nextSibling;
|
||||
// If we can't go down, try to go back up the tree.
|
||||
if (!nextNode) {
|
||||
nextNode = node.parentNode;
|
||||
while (nextNode) {
|
||||
// We are valid again!
|
||||
if (nextNode.nextSibling) {
|
||||
nextNode = nextNode.nextSibling;
|
||||
break;
|
||||
}
|
||||
// Try going up again. If parentNode is null that means we have
|
||||
// reached the top, and we will break out of both loops.
|
||||
nextNode = nextNode.parentNode;
|
||||
}
|
||||
}
|
||||
node = nextNode;
|
||||
}
|
||||
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
|
||||
* @package
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @package
|
||||
*/
|
||||
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,10 @@ Blockly.ZoomControls.prototype.dispose = function() {
|
||||
* Move the zoom controls to the bottom-right corner.
|
||||
*/
|
||||
Blockly.ZoomControls.prototype.position = function() {
|
||||
// Not yet initialized.
|
||||
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