Comments v3 (#1817)

* Add skeleton for workspace comments

* XML parsing and encoding of workspace comments.

* Minor fix: piping the height and width from xml to newWorkspaceComment

* Move height and width into workspace_comment_svg

* rename newWorkspaceComment to newComment

* minor refactoring. PR changes

* Functions for managing the comment's lifecycle

* Add initial tests

* Add another test

* Add basic rendering of a comment.

* Cleanup remaining highlighting steps from render

* Fix lint

* Fix aslant

* Add basic comment translate

* Simplify render code into one setPath method

* Move steps to setPath_

* Remove svg elements when disposing of a comment; some code cleanup

* Add a workspace comment on context menu click and position it where the initial context menu was clicked.

* Minor rendering changes, fixes RTL. Fix positioning of new (context menu) comments while workspace is scaled.

* PR feedback

* Gesture code for dragging comments

* Add comment (block drag) surface methods

* minor comment fix

* Comment fixes

* Add comment dragger

* Making rendered private

* Require CommentDragger

* Make basic comment dragging work

* Increase the border around the comment to make a bigger drag handle

* Remove typo

* Allow comments to be selected. Highlight selected comment. Only edit comment on click. Updated comment rendering.

* minor refactor: remove commented out code

* PR comments

* lint and rebuild

* Fix renamed function call

* Fix workspace getMetrics by storing comment size as a number, not a string

* Enable comment deletion when dragging over the toolbox or trash can

* Give issue references to some todos

* Create a helper function for workspace comment creation

* Integrate sam's workspace comments, using the bubble dragger

* Remove comment_dragger references

* Remove comment dragger.js

* Remove pointer handling

* Fix lint

* Move comment XML functions into the comment files.

* Fix tests

* Fix type annotations

* Fix comments on comments

* Fix compiler errors related to visibility.

* Fix merge issues and add an issue number to a TODO

* Add a new message for default text on workspace comments, and rebuild

* Add support for a context menu on workspace comment showing delete and duplication options.
Add copy and paste support.

* PR comment feedback

* Show a delete icon on the comment when selected. Delete icon deletes the comment. Comment can be deleted if dragged onto the toolbox or the trash icon. A normal bubble cannot be deleted that way.

* use isDeletable instead

* Support drag of the comment during editing mode using the top handle.

* Add skeletons for all workspace comment events

* Rebuild with new comments

* Get rid of confused TODO

* JSDoc on a function

* Fix broken tests

* More PR feedback

* Fix lint

* Delete comment on mouse out, highlight on mouse down.

* Fix lint.

* Show delete hand cursor when dragging a comment to delete over the toolbox

* Focus textarea on select

* Add delete events

* Remove workspace comment create event, and add TODO placeholder

* Provide default values if comment height and width are missing in XML

* Set comment handle fill to none by default

* Rebuild

* Comment de/serialization should include location.

* Add comment move events, with undo and redo

* Add comment change events

* Move files up to core

* Add package/private annotations wherever possible

* Move the workspace comment events up to core and into a single file

* Mark things package or private where possible

* Get rid of unnecessary changes to messge files

* Fix lint

* Fix some review feedback

* Make changes to the comment db happen in addTopComment and removeTopComment

* Add css classes for toggling comment focus

* Clean up css for comment focus

* Rebuild
This commit is contained in:
Rachel Fenichel
2018-04-27 15:18:59 -07:00
committed by GitHub
parent a6e386d14f
commit ee6f2ea097
20 changed files with 2688 additions and 158 deletions

View File

@@ -246,35 +246,41 @@ Blockly.onKeyDown_ = function(e) {
};
/**
* Copy a block onto the local clipboard.
* @param {!Blockly.Block} block Block to be copied.
* Copy a block or workspace comment onto the local clipboard.
* @param {!Blockly.Block | !Blockly.WorkspaceComment} toCopy Block or Workspace Comment
* to be copied.
* @private
*/
Blockly.copy_ = function(block) {
var xmlBlock = Blockly.Xml.blockToDom(block);
// Copy only the selected block and internal blocks.
Blockly.Xml.deleteNext(xmlBlock);
// Encode start position in XML.
var xy = block.getRelativeToSurfaceXY();
xmlBlock.setAttribute('x', block.RTL ? -xy.x : xy.x);
xmlBlock.setAttribute('y', xy.y);
Blockly.clipboardXml_ = xmlBlock;
Blockly.clipboardSource_ = block.workspace;
Blockly.copy_ = function(toCopy) {
if (toCopy.isComment) {
var xml = toCopy.toXmlWithXY();
} else {
var xml = Blockly.Xml.blockToDom(toCopy);
// Copy only the selected block and internal blocks.
Blockly.Xml.deleteNext(xml);
// Encode start position in XML.
var xy = toCopy.getRelativeToSurfaceXY();
xml.setAttribute('x', toCopy.RTL ? -xy.x : xy.x);
xml.setAttribute('y', xy.y);
}
Blockly.clipboardXml_ = xml;
Blockly.clipboardSource_ = toCopy.workspace;
};
/**
* Duplicate this block and its children.
* @param {!Blockly.Block} block Block to be copied.
* Duplicate this block and its children, or a workspace comment.
* @param {!Blockly.Block | !Blockly.WorkspaceComment} toDuplicate Block or
* Workspace Comment to be copied.
* @private
*/
Blockly.duplicate_ = function(block) {
Blockly.duplicate_ = function(toDuplicate) {
// Save the clipboard.
var clipboardXml = Blockly.clipboardXml_;
var clipboardSource = Blockly.clipboardSource_;
// Create a duplicate via a copy/paste operation.
Blockly.copy_(block);
block.workspace.paste(Blockly.clipboardXml_);
Blockly.copy_(toDuplicate);
toDuplicate.workspace.paste(Blockly.clipboardXml_);
// Restore the clipboard.
Blockly.clipboardXml_ = clipboardXml;

View File

@@ -304,6 +304,25 @@ Blockly.Bubble.prototype.bubbleMouseDown_ = function(e) {
}
};
/**
* Show the context menu for this bubble.
* @param {!Event} _e Mouse event.
* @private
*/
Blockly.Bubble.prototype.showContextMenu_ = function(_e) {
// NOP on bubbles, but used by the bubble dragger to pass events to
// workspace comments.
};
/**
* Get whether this bubble is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
Blockly.Bubble.prototype.isDeletable = function() {
return false;
};
/**
* Handle a mouse-down on bubble's resize corner.
* @param {!Event} e Mouse down event.

View File

@@ -26,21 +26,27 @@
goog.provide('Blockly.BubbleDragger');
goog.require('Blockly.Bubble');
goog.require('Blockly.Events.CommentMove');
goog.require('Blockly.WorkspaceCommentSvg');
goog.require('goog.math.Coordinate');
goog.require('goog.asserts');
/**
* Class for a bubble dragger. It moves bubbles around the workspace when they
* are being dragged by a mouse or touch.
* @param {!Blockly.Bubble} bubble The bubble to drag.
* Class for a bubble dragger. It moves things on the bubble canvas around the
* workspace when they are being dragged by a mouse or touch. These can be
* block comments, mutators, warnings, or workspace comments.
* @param {!Blockly.Bubble|!Blockly.WorkspaceCommentSvg} bubble The item on the
* bubble canvas to drag.
* @param {!Blockly.WorkspaceSvg} workspace The workspace to drag on.
* @constructor
*/
Blockly.BubbleDragger = function(bubble, workspace) {
/**
* The bubble that is being dragged.
* @type {!Blockly.Bubble}
* The item on the bubble canvas that is being dragged.
* @type {!Blockly.Bubble|!Blockly.WorkspaceCommentSvg}
* @private
*/
this.draggingBubble_ = bubble;
@@ -52,6 +58,22 @@ Blockly.BubbleDragger = function(bubble, workspace) {
*/
this.workspace_ = workspace;
/**
* Which delete area the mouse pointer is over, if any.
* One of {@link Blockly.DELETE_AREA_TRASH},
* {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
* @type {?number}
* @private
*/
this.deleteArea_ = null;
/**
* Whether the bubble would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBubble_ = false;
/**
* The location of the top left corner of the dragging bubble's body at the
* beginning of the drag, in workspace coordinates.
@@ -95,6 +117,15 @@ Blockly.BubbleDragger.prototype.startBubbleDrag = function() {
if (this.dragSurface_) {
this.moveToDragSurface_();
}
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
var toolbox = this.workspace_.getToolbox();
if (toolbox) {
var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
toolbox.addStyle(style);
}
};
/**
@@ -110,8 +141,55 @@ Blockly.BubbleDragger.prototype.dragBubble = function(e, currentDragDeltaXY) {
var newLoc = goog.math.Coordinate.sum(this.startXY_, delta);
this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
// TODO (fenichel): Possibly update the cursor if dragging to the trash can
// is allowed.
if (this.draggingBubble_.isDeletable()) {
this.deleteArea_ = this.workspace_.isDeleteArea(e);
this.updateCursorDuringBubbleDrag_();
}
};
/**
* Shut the trash can and, if necessary, delete the dragging bubble.
* Should be called at the end of a bubble drag.
* @return {boolean} whether the bubble was deleted.
* @private
*/
Blockly.BubbleDragger.prototype.maybeDeleteBubble_ = function() {
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBubble_) {
if (trashcan) {
goog.Timer.callOnce(trashcan.close, 100, trashcan);
}
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBubble_.dispose(false, true);
} else if (trashcan) {
// Make sure the trash can is closed.
trashcan.close();
}
return this.wouldDeleteBubble_;
};
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging bubble would be deleted if released immediately.
* @private
*/
Blockly.BubbleDragger.prototype.updateCursorDuringBubbleDrag_ = function() {
this.wouldDeleteBubble_ = this.deleteArea_ != Blockly.DELETE_AREA_NONE;
var trashcan = this.workspace_.trashcan;
if (this.wouldDeleteBubble_) {
this.draggingBubble_.setDeleteStyle(true);
if (this.deleteArea_ == Blockly.DELETE_AREA_TRASH && trashcan) {
trashcan.setOpen_(true);
}
} else {
this.draggingBubble_.setDeleteStyle(false);
if (trashcan) {
trashcan.setOpen_(false);
}
}
};
/**
@@ -131,14 +209,24 @@ Blockly.BubbleDragger.prototype.endBubbleDrag = function(
// Move the bubble to its final location.
this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
// Put everything back onto the bubble canvas.
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
}
var deleted = this.maybeDeleteBubble_();
this.fireMoveEvent_();
if (!deleted) {
// Put everything back onto the bubble canvas.
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
}
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(false);
this.fireMoveEvent_();
}
this.workspace_.setResizesEnabled(true);
if (this.workspace_.toolbox_) {
var style = this.draggingBubble_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
this.workspace_.toolbox_.removeStyle(style);
}
Blockly.Events.setGroup(false);
};
@@ -147,6 +235,12 @@ Blockly.BubbleDragger.prototype.endBubbleDrag = function(
* @private
*/
Blockly.BubbleDragger.prototype.fireMoveEvent_ = function() {
if (this.draggingBubble_.isComment) {
var event = new Blockly.Events.CommentMove(this.draggingBubble_);
event.setOldCoordinate(this.startXY_);
event.recordNew();
Blockly.Events.fire(event);
}
// TODO (fenichel): move events for comments.
return;
};

View File

@@ -300,3 +300,98 @@ Blockly.ContextMenu.blockCommentOption = function(block) {
}
return commentOption;
};
/**
* Make a context menu option for deleting the current workspace comment.
* @param {!Blockly.WorkspaceCommentSvg} comment The workspace comment where the
* right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.commentDeleteOption = function(comment) {
var deleteOption = {
text: Blockly.Msg.REMOVE_COMMENT,
enabled: true,
callback: function() {
Blockly.Events.setGroup(true);
comment.dispose(true, true);
Blockly.Events.setGroup(false);
}
};
return deleteOption;
};
/**
* Make a context menu option for duplicating the current workspace comment.
* @param {!Blockly.WorkspaceCommentSvg} comment The workspace comment where the
* right-click originated.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.commentDuplicateOption = function(comment) {
var duplicateOption = {
text: Blockly.Msg.DUPLICATE_COMMENT,
enabled: true,
callback: function() {
Blockly.duplicate_(comment);
}
};
return duplicateOption;
};
/**
* Make a context menu option for adding a comment on the workspace.
* @param {!Blockly.WorkspaceSvg} ws The workspace where the right-click
* originated.
* @param {!Event} e The right-click mouse event.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
*/
Blockly.ContextMenu.workspaceCommentOption = function(ws, e) {
// Helper function to create and position a comment correctly based on the
// location of the mouse event.
var addWsComment = function() {
var comment = new Blockly.WorkspaceCommentSvg(
ws, Blockly.Msg.WORKSPACE_COMMENT_DEFAULT_TEXT,
Blockly.WorkspaceCommentSvg.DEFAULT_SIZE,
Blockly.WorkspaceCommentSvg.DEFAULT_SIZE);
var injectionDiv = ws.getInjectionDiv();
// Bounding rect coordinates are in client coordinates, meaning that they
// are in pixels relative to the upper left corner of the visible browser
// window. These coordinates change when you scroll the browser window.
var boundingRect = injectionDiv.getBoundingClientRect();
// The client coordinates offset by the injection div's upper left corner.
var clientOffsetPixels = new goog.math.Coordinate(
e.clientX - boundingRect.left, e.clientY - boundingRect.top);
// The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div.
var mainOffsetPixels = ws.getOriginOffsetInPixels();
// The position of the new comment in pixels relative to the origin of the
// main workspace.
var finalOffsetPixels = goog.math.Coordinate.difference(clientOffsetPixels,
mainOffsetPixels);
// The position of the new comment in main workspace coordinates.
var finalOffsetMainWs = finalOffsetPixels.scale(1 / ws.scale);
var commentX = finalOffsetMainWs.x;
var commentY = finalOffsetMainWs.y;
comment.moveBy(commentX, commentY);
if (ws.rendered) {
comment.initSvg();
comment.render(false);
comment.select();
}
};
var wsCommentOption = {enabled: true};
wsCommentOption.text = Blockly.Msg.ADD_COMMENT;
wsCommentOption.callback = function() {
addWsComment();
};
return wsCommentOption;
};

View File

@@ -189,7 +189,7 @@ Blockly.Css.CONTENT = [
'}',
'.blocklyResizeLine {',
'stroke: #888;',
'stroke: #515A5A;',
'stroke-width: 1;',
'}',
@@ -383,17 +383,76 @@ Blockly.Css.CONTENT = [
'padding: 0;',
'}',
'.blocklyCommentForeignObject {',
'position: relative;',
'z-index: 0;',
'}',
'.blocklyCommentRect {',
'fill: #E7DE8E;',
'stroke: #bcA903;',
'stroke-width: 1px',
'}',
'.blocklyCommentTarget {',
'fill: transparent;',
'stroke: #bcA903;',
'}',
'.blocklyCommentTargetFocused {',
'fill: none;',
'}',
'.blocklyCommentHandleTarget {',
'fill: none;',
'}',
'.blocklyCommentHandleTargetFocused {',
'fill: transparent;',
'}',
'.blocklyFocused>.blocklyCommentRect {',
'fill: #B9B272;',
'stroke: #B9B272;',
'}',
'.blocklySelected>.blocklyCommentTarget {',
'stroke: #fc3;',
'stroke-width: 3px;',
'}',
'.blocklyCommentTextarea {',
'background-color: #ffc;',
'background-color: #fef49c;',
'border: 0;',
'outline: 0;',
'margin: 0;',
'padding: 2px;',
'padding: 3px;',
'resize: none;',
'display: block;',
'overflow: hidden;',
'}',
'.blocklyCommentDeleteIcon {',
'cursor: pointer;',
'fill: #000;',
'display: none',
'}',
'.blocklySelected > .blocklyCommentDeleteIcon {',
'display: block',
'}',
'.blocklyDeleteIconShape {',
'fill: #000;',
'stroke: #000;',
'stroke-width: 1px;',
'}',
'.blocklyDeleteIconShape.blocklyDeleteIconHighlighted {',
'stroke: #fc3;',
'}',
'.blocklyHtmlInput {',
'border: none;',
'border-radius: 4px;',

View File

@@ -126,6 +126,30 @@ Blockly.Events.VAR_RENAME = 'var_rename';
*/
Blockly.Events.UI = 'ui';
/**
* Name of event that creates a comment.
* @const
*/
Blockly.Events.COMMENT_CREATE = 'comment_create';
/**
* Name of event that deletes a comment.
* @const
*/
Blockly.Events.COMMENT_DELETE = 'comment_delete';
/**
* Name of event that changes a comment.
* @const
*/
Blockly.Events.COMMENT_CHANGE = 'comment_change';
/**
* Name of event that moves a comment.
* @const
*/
Blockly.Events.COMMENT_MOVE = 'comment_move';
/**
* List of events queued for firing.
* @private
@@ -328,6 +352,18 @@ Blockly.Events.fromJson = function(json, workspace) {
case Blockly.Events.UI:
event = new Blockly.Events.Ui(null);
break;
case Blockly.Events.COMMENT_CREATE:
event = new Blockly.Events.CommentCreate(null);
break;
case Blockly.Events.COMMENT_CHANGE:
event = new Blockly.Events.CommentChange(null);
break;
case Blockly.Events.COMMENT_MOVE:
event = new Blockly.Events.CommentMove(null);
break;
case Blockly.Events.COMMENT_DELETE:
event = new Blockly.Events.CommentDelete(null);
break;
default:
throw 'Unknown event type.';
}

View File

@@ -469,7 +469,6 @@ Blockly.Gesture.prototype.startDraggingBubble_ = function() {
this.bubbleDragger_.dragBubble(this.mostRecentEvent_,
this.currentDragDeltaXY_);
};
/**
* Start a gesture: update the workspace to indicate that a gesture is in
* progress and bind mousemove and mouseup handlers.
@@ -628,6 +627,8 @@ Blockly.Gesture.prototype.handleRightClick = function(e) {
this.bringBlockToFront_();
Blockly.hideChaff(this.flyout_);
this.targetBlock_.showContextMenu_(e);
} else if (this.startBubble_) {
this.startBubble_.showContextMenu_(e);
} else if (this.startWorkspace_ && !this.flyout_) {
Blockly.hideChaff();
this.startWorkspace_.showContextMenu_(e);
@@ -706,8 +707,9 @@ Blockly.Gesture.prototype.handleBubbleStart = function(e, bubble) {
* @private
*/
Blockly.Gesture.prototype.doBubbleClick_ = function() {
// TODO: This isn't really enough, is it.
this.startBubble_.promote_();
// TODO (#1673): Consistent handling of single clicks.
this.startBubble_.setFocus && this.startBubble_.setFocus();
this.startBubble_.select && this.startBubble_.select();
};
/**
@@ -752,6 +754,7 @@ Blockly.Gesture.prototype.doWorkspaceClick_ = function() {
}
};
/* End functions defining what actions to take to execute clicks on each type
* of target. */
@@ -802,7 +805,8 @@ Blockly.Gesture.prototype.setStartBubble = function(bubble) {
* @package
*/
Blockly.Gesture.prototype.setStartBlock = function(block) {
if (!this.startBlock_) {
// If the gesture already went through a bubble, don't set the start block.
if (!this.startBlock_ && !this.startBubble_) {
this.startBlock_ = block;
if (block.isInFlyout && block != block.getRootBlock()) {
this.setTargetBlock_(block.getRootBlock());
@@ -849,6 +853,7 @@ Blockly.Gesture.prototype.setStartFlyout_ = function(flyout) {
}
};
/* End functions for populating a gesture at mouse down. */
/* Begin helper functions defining types of clicks. Any developer wanting
@@ -899,7 +904,8 @@ Blockly.Gesture.prototype.isFieldClick_ = function() {
* @private
*/
Blockly.Gesture.prototype.isWorkspaceClick_ = function() {
var onlyTouchedWorkspace = !this.startBlock_ && !this.startField_;
var onlyTouchedWorkspace = !this.startBlock_ && !this.startBubble_ &&
!this.startField_;
return onlyTouchedWorkspace && !this.hasExceededDragRadius_;
};

View File

@@ -27,6 +27,7 @@
goog.provide('Blockly.Workspace');
goog.require('Blockly.VariableMap');
goog.require('Blockly.WorkspaceComment');
goog.require('goog.array');
goog.require('goog.math');
@@ -55,6 +56,16 @@ Blockly.Workspace = function(opt_options) {
* @private
*/
this.topBlocks_ = [];
/**
* @type {!Array.<!Blockly.WorkspaceComment>}
* @private
*/
this.topComments_ = [];
/**
* @type {!Object}
* @private
*/
this.commentDB_ = Object.create(null);
/**
* @type {!Array.<!Function>}
* @private
@@ -170,6 +181,61 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
return blocks;
};
/**
* Add a comment to the list of top comments.
* @param {!Blockly.WorkspaceComment} comment comment to add.
* @package
*/
Blockly.Workspace.prototype.addTopComment = function(comment) {
this.topComments_.push(comment);
// Note: If the comment database starts to hold block comments, this may need
// to move to a separate function.
if (this.commentDB_[comment.id]) {
console.warn('Overriding an existing comment on this workspace, with id "' +
comment.id + '"');
}
this.commentDB_[comment.id] = comment;
};
/**
* Remove a comment from the list of top comments.
* @param {!Blockly.WorkspaceComment} comment comment to remove.
* @package
*/
Blockly.Workspace.prototype.removeTopComment = function(comment) {
if (!goog.array.remove(this.topComments_, comment)) {
throw 'Comment not present in workspace\'s list of top-most comments.';
}
// Note: If the comment database starts to hold block comments, this may need
// to move to a separate function.
delete this.commentDB_[comment.id];
};
/**
* Finds the top-level comments and returns them. Comments are optionally sorted
* by position; top to bottom (with slight LTR or RTL bias).
* @param {boolean} ordered Sort the list if true.
* @return {!Array.<!Blockly.WorkspaceComment>} The top-level comment objects.
* @package
*/
Blockly.Workspace.prototype.getTopComments = function(ordered) {
// Copy the topComments_ list.
var comments = [].concat(this.topComments_);
if (ordered && comments.length > 1) {
var offset = Math.sin(goog.math.toRadians(Blockly.Workspace.SCAN_ANGLE));
if (this.RTL) {
offset *= -1;
}
comments.sort(function(a, b) {
var aXY = a.getRelativeToSurfaceXY();
var bXY = b.getRelativeToSurfaceXY();
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
});
}
return comments;
};
/**
* Find all blocks in workspace. Blocks are optionally sorted
* by position; top to bottom (with slight LTR or RTL bias).
@@ -195,7 +261,7 @@ Blockly.Workspace.prototype.getAllBlocks = function(ordered) {
};
/**
* Dispose of all blocks in workspace.
* Dispose of all blocks and comments in workspace.
*/
Blockly.Workspace.prototype.clear = function() {
var existingGroup = Blockly.Events.getGroup();
@@ -205,6 +271,9 @@ Blockly.Workspace.prototype.clear = function() {
while (this.topBlocks_.length) {
this.topBlocks_[0].dispose();
}
while (this.topComments_.length) {
this.topComments_[this.topComments_.length - 1].dispose();
}
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
@@ -458,6 +527,17 @@ Blockly.Workspace.prototype.getBlockById = function(id) {
return this.blockDB_[id] || null;
};
/**
* Find the comment on this workspace with the specified ID.
* @param {string} id ID of comment to find.
* @return {Blockly.WorkspaceComment} The sought after comment or null if not
* found.
* @package
*/
Blockly.Workspace.prototype.getCommentById = function(id) {
return this.commentDB_[id] || null;
};
/**
* Checks whether all value and statement inputs in the workspace are filled
* with blocks.

370
core/workspace_comment.js Normal file
View File

@@ -0,0 +1,370 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object representing a code comment on the workspace.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceComment');
goog.require('Blockly.Events.CommentChange');
goog.require('Blockly.Events.CommentCreate');
goog.require('Blockly.Events.CommentDelete');
goog.require('Blockly.Events.CommentMove');
goog.require('goog.math.Coordinate');
/**
* Class for a workspace comment.
* @param {!Blockly.Workspace} workspace The block's workspace.
* @param {string} content The content of this workspace comment.
* @param {number} height Height of the comment.
* @param {number} width Width of the comment.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @constructor
*/
Blockly.WorkspaceComment = function(workspace, content, height, width, opt_id) {
/** @type {string} */
this.id = (opt_id && !workspace.getCommentById(opt_id)) ?
opt_id : Blockly.utils.genUid();
workspace.addTopComment(this);
/**
* The comment's position in workspace units. (0, 0) is at the workspace's
* origin; scale does not change this value.
* @type {!goog.math.Coordinate}
* @protected
*/
this.xy_ = new goog.math.Coordinate(0, 0);
/**
* The comment's height in workspace units. Scale does not change this value.
* @type {number}
* @private
*/
this.height_ = height;
/**
* The comment's width in workspace units. Scale does not change this value.
* @type {number}
* @private
*/
this.width_ = width;
/**
* @type {!Blockly.Workspace}
*/
this.workspace = workspace;
/**
* @protected
* @type {boolean}
*/
this.RTL = workspace.RTL;
/**
* @type {boolean}
* @private
*/
this.deletable_ = true;
/**
* @type {boolean}
* @private
*/
this.movable_ = true;
/**
* @protected
* @type {!string}
*/
this.content_ = content;
/**
* @package
* @type {boolean}
*/
this.isComment = true;
Blockly.WorkspaceComment.fireCreateEvent(this);
};
/**
* Dispose of this comment.
* @package
*/
Blockly.WorkspaceComment.prototype.dispose = function() {
if (!this.workspace) {
// The comment has already been deleted.
return;
}
if (Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.CommentDelete(this));
}
// Remove from the list of top comments and the comment database.
this.workspace.removeTopComment(this);
this.workspace = null;
};
// Height, width, x, and y are all stored on even non-rendered comments, to
// preserve state if you pass the contents through a headless workspace.
/**
* Get comment height.
* @return {number} comment height.
* @package
*/
Blockly.WorkspaceComment.prototype.getHeight = function() {
return this.height_;
};
/**
* Set comment height.
* @param {number} height comment height.
* @package
*/
Blockly.WorkspaceComment.prototype.setHeight = function(height) {
this.height_ = height;
};
/**
* Get comment width.
* @return {number} comment width.
* @package
*/
Blockly.WorkspaceComment.prototype.getWidth = function() {
return this.width_;
};
/**
* Set comment width.
* @param {number} width comment width.
* @package
*/
Blockly.WorkspaceComment.prototype.setWidth = function(width) {
this.width_ = width;
};
/**
* Get stored location.
* @return {!goog.math.Coordinate} The comment's stored location. This is not
* valid if the comment is currently being dragged.
* @package
*/
Blockly.WorkspaceComment.prototype.getXY = function() {
return this.xy_.clone();
};
/**
* Move a comment by a relative offset.
* @param {number} dx Horizontal offset, in workspace units.
* @param {number} dy Vertical offset, in workspace units.
* @package
*/
Blockly.WorkspaceComment.prototype.moveBy = function(dx, dy) {
var event = new Blockly.Events.CommentMove(this);
this.xy_.translate(dx, dy);
event.recordNew();
Blockly.Events.fire(event);
};
/**
* Get whether this comment is deletable or not.
* @return {boolean} True if deletable.
* @package
*/
Blockly.WorkspaceComment.prototype.isDeletable = function() {
return this.deletable_ &&
!(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is deletable or not.
* @param {boolean} deletable True if deletable.
* @package
*/
Blockly.WorkspaceComment.prototype.setDeletable = function(deletable) {
this.deletable_ = deletable;
};
/**
* Get whether this comment is movable or not.
* @return {boolean} True if movable.
* @package
*/
Blockly.WorkspaceComment.prototype.isMovable = function() {
return this.movable_ &&
!(this.workspace && this.workspace.options.readOnly);
};
/**
* Set whether this comment is movable or not.
* @param {boolean} movable True if movable.
* @package
*/
Blockly.WorkspaceComment.prototype.setMovable = function(movable) {
this.movable_ = movable;
};
/**
* Returns this comment's text.
* @return {string} Comment text.
* @package
*/
Blockly.WorkspaceComment.prototype.getContent = function() {
return this.content_;
};
/**
* Set this comment's content.
* @param {string} content Comment content.
* @package
*/
Blockly.WorkspaceComment.prototype.setContent = function(content) {
if (this.content_ != content) {
Blockly.Events.fire(
new Blockly.Events.CommentChange(this, this.content_, content));
this.content_ = content;
}
};
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.WorkspaceComment.prototype.toXmlWithXY = function(opt_noId) {
var element = this.toXml(opt_noId);
element.setAttribute('x', Math.round(this.xy_.x));
element.setAttribute('y', Math.round(this.xy_.y));
element.setAttribute('h', this.height_);
element.setAttribute('w', this.width_);
return element;
};
/**
* Encode a comment subtree as XML, but don't serialize the XY coordinates.
* This method avoids some expensive metrics-related calls that are made in
* toXmlWithXY().
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.WorkspaceComment.prototype.toXml = function(opt_noId) {
var commentElement = goog.dom.createDom('comment');
if (!opt_noId) {
commentElement.setAttribute('id', this.id);
}
commentElement.textContent = this.getContent();
return commentElement;
};
/**
* Fire a create event for the given workspace comment, if comments are enabled.
* @param {!Blockly.WorkspaceComment} comment The comment that was just created.
* @package
*/
Blockly.WorkspaceComment.fireCreateEvent = function(comment) {
if (Blockly.Events.isEnabled()) {
var existingGroup = Blockly.Events.getGroup();
if (!existingGroup) {
Blockly.Events.setGroup(true);
}
try {
Blockly.Events.fire(new Blockly.Events.CommentCreate(comment));
} finally {
if (!existingGroup) {
Blockly.Events.setGroup(false);
}
}
}
};
/**
* Decode an XML comment tag and create a comment on the workspace.
* @param {!Element} xmlComment XML comment element.
* @param {!Blockly.Workspace} workspace The workspace.
* @return {!Blockly.WorkspaceComment} The created workspace comment.
* @package
*/
Blockly.WorkspaceComment.fromXml = function(xmlComment, workspace) {
var info = Blockly.WorkspaceComment.parseAttributes(xmlComment);
var comment = new Blockly.WorkspaceComment(
workspace, info.content, info.h, info.w, info.id);
var commentX = parseInt(xmlComment.getAttribute('x'), 10);
var commentY = parseInt(xmlComment.getAttribute('y'), 10);
if (!isNaN(commentX) && !isNaN(commentY)) {
comment.moveBy(commentX, commentY);
}
Blockly.WorkspaceComment.fireCreateEvent(comment);
return comment;
};
/**
* Decode an XML comment tag and return the results in an object.
* @param {!Element} xml XML comment element.
* @return {!Object} An object containing the information about the comment.
* @package
*/
Blockly.WorkspaceComment.parseAttributes = function(xml) {
var xmlH = xml.getAttribute('h');
var xmlW = xml.getAttribute('w');
return {
/* @type {string} */
id: xml.getAttribute('id'),
/**
* The height of the comment in workspace units, or 100 if not specified.
* @type {number}
*/
h: xmlH ? parseInt(xmlH, 10) : 100,
/**
* The width of the comment in workspace units, or 100 if not specified.
* @type {number}
*/
w: xmlW ? parseInt(xmlW, 10) : 100,
/**
* The x position of the comment in workspace coordinates, or NaN if not
* specified in the XML.
* @type {number}
*/
x: parseInt(xml.getAttribute('x'), 10),
/**
* The y position of the comment in workspace coordinates, or NaN if not
* specified in the XML.
* @type {number}
*/
y: parseInt(xml.getAttribute('y'), 10),
/* @type {string} */
content: xml.textContent
};
};

View File

@@ -0,0 +1,463 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Methods for rendering a workspace comment as SVG
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceCommentSvg.render');
goog.require('Blockly.WorkspaceCommentSvg');
/**
* Size of the resize icon.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.RESIZE_SIZE = 8;
/**
* Radius of the border around the comment.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.BORDER_RADIUS = 3;
/**
* Offset from the foreignobject edge to the textarea edge.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET = 2;
/**
* Offset from the top to make room for a top bar.
* @type {number}
* @const
* @private
*/
Blockly.WorkspaceCommentSvg.TOP_OFFSET = 10;
/**
* Returns a bounding box describing the dimensions of this comment.
* @return {!{height: number, width: number}} Object with height and width
* properties in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getHeightWidth = function() {
return { width: this.getWidth(), height: this.getHeight() };
};
/**
* Renders the workspace comment.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.render = function() {
if (this.rendered_) {
return;
}
var size = this.getHeightWidth();
// Add text area
this.createEditor_();
this.svgGroup_.appendChild(this.foreignObject_);
this.svgHandleTarget_ = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyCommentHandleTarget',
'x': 0,
'y': 0
});
this.svgGroup_.appendChild(this.svgHandleTarget_);
this.svgRectTarget_ = Blockly.utils.createSvgElement('rect',
{
'class': 'blocklyCommentTarget',
'x': 0,
'y': 0,
'rx': Blockly.WorkspaceCommentSvg.BORDER_RADIUS,
'ry': Blockly.WorkspaceCommentSvg.BORDER_RADIUS
});
this.svgGroup_.appendChild(this.svgRectTarget_);
// Add the resize icon
this.addResizeDom_();
if (this.isDeletable()) {
// Add the delete icon
this.addDeleteDom_();
}
this.setSize_(size.width, size.height);
// Set the content
this.textarea_.value = this.content_;
this.rendered_ = true;
if (this.resizeGroup_) {
Blockly.bindEventWithChecks_(
this.resizeGroup_, 'mousedown', this, this.resizeMouseDown_);
}
if (this.isDeletable()) {
Blockly.bindEventWithChecks_(
this.deleteGroup_, 'mousedown', this, this.deleteMouseDown_);
Blockly.bindEventWithChecks_(
this.deleteGroup_, 'mouseout', this, this.deleteMouseOut_);
Blockly.bindEventWithChecks_(
this.deleteGroup_, 'mouseup', this, this.deleteMouseUp_);
}
};
/**
* Create the text area for the comment.
* @return {!Element} The top-level node of the editor.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.createEditor_ = function() {
/* Create the editor. Here's the markup that will be generated:
<foreignObject class="blocklyCommentForeignObject" x="0" y="10" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
*/
this.foreignObject_ = Blockly.utils.createSvgElement(
'foreignObject',
{
'x': 0,
'y': Blockly.WorkspaceCommentSvg.TOP_OFFSET,
'class': 'blocklyCommentForeignObject'
},
null);
var body = document.createElementNS(Blockly.HTML_NS, 'body');
body.setAttribute('xmlns', Blockly.HTML_NS);
body.className = 'blocklyMinimalBody';
var textarea = document.createElementNS(Blockly.HTML_NS, 'textarea');
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.RTL ? 'RTL' : 'LTR');
body.appendChild(textarea);
this.textarea_ = textarea;
this.foreignObject_.appendChild(body);
// Don't zoom with mousewheel.
Blockly.bindEventWithChecks_(textarea, 'wheel', this, function(e) {
e.stopPropagation();
});
Blockly.bindEventWithChecks_(textarea, 'change', this, function(
/* eslint-disable no-unused-vars */ e
/* eslint-enable no-unused-vars */) {
this.setContent(textarea.value);
});
return this.foreignObject_;
};
/**
* Add the resize icon to the DOM
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.addResizeDom_ = function() {
this.resizeGroup_ = Blockly.utils.createSvgElement(
'g',
{
'class': this.RTL ? 'blocklyResizeSW' : 'blocklyResizeSE'
},
this.svgGroup_);
var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE;
Blockly.utils.createSvgElement(
'polygon',
{'points': '0,x x,x x,0'.replace(/x/g, resizeSize.toString())},
this.resizeGroup_);
Blockly.utils.createSvgElement(
'line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize / 3
}, this.resizeGroup_);
Blockly.utils.createSvgElement(
'line',
{
'class': 'blocklyResizeLine',
'x1': resizeSize * 2 / 3, 'y1': resizeSize - 1,
'x2': resizeSize - 1, 'y2': resizeSize * 2 / 3
}, this.resizeGroup_);
};
/**
* Add the delete icon to the DOM
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.addDeleteDom_ = function() {
this.deleteGroup_ = Blockly.utils.createSvgElement(
'g',
{
'class': 'blocklyCommentDeleteIcon'
},
this.svgGroup_);
this.deleteIconBorder_ = Blockly.utils.createSvgElement('circle',
{
'class': 'blocklyDeleteIconShape',
'r': '7',
'cx': '7.5',
'cy': '7.5'
},
this.deleteGroup_);
// x icon.
Blockly.utils.createSvgElement(
'line',
{
'x1': '5', 'y1': '10',
'x2': '10', 'y2': '5',
'stroke': '#fff',
'stroke-width': '2'
},
this.deleteGroup_);
Blockly.utils.createSvgElement(
'line',
{
'x1': '5', 'y1': '5',
'x2': '10', 'y2': '10',
'stroke': '#fff',
'stroke-width': '2'
},
this.deleteGroup_);
};
/**
* Handle a mouse-down on comment's resize corner.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeMouseDown_ = function(e) {
//this.promote_();
this.unbindDragEvents_();
if (Blockly.utils.isRightButton(e)) {
// No right-click.
e.stopPropagation();
return;
}
// Left-click (or middle click)
this.workspace.startDrag(e, new goog.math.Coordinate(
this.workspace.RTL ? -this.width_ : this.width_, this.height_));
this.onMouseUpWrapper_ = Blockly.bindEventWithChecks_(
document, 'mouseup', this, this.resizeMouseUp_);
this.onMouseMoveWrapper_ = Blockly.bindEventWithChecks_(
document, 'mousemove', this, this.resizeMouseMove_);
Blockly.hideChaff();
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Handle a mouse-down on comment's delete icon.
* @param {!Event} e Mouse down event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.deleteMouseDown_ = function(e) {
// highlight the delete icon
Blockly.utils.addClass(
/** @type {!Element} */ (this.deleteIconBorder_), 'blocklyDeleteIconHighlighted');
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Handle a mouse-out on comment's delete icon.
* @param {!Event} e Mouse out event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.deleteMouseOut_ = function(/*e*/) {
// restore highlight on the delete icon
Blockly.utils.removeClass(
/** @type {!Element} */ (this.deleteIconBorder_), 'blocklyDeleteIconHighlighted');
};
/**
* Handle a mouse-up on comment's delete icon.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.deleteMouseUp_ = function(e) {
// Delete this comment
this.dispose(true, true);
// This event has been handled. No need to bubble up to the document.
e.stopPropagation();
};
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.unbindDragEvents_ = function() {
if (this.onMouseUpWrapper_) {
Blockly.unbindEvent_(this.onMouseUpWrapper_);
this.onMouseUpWrapper_ = null;
}
if (this.onMouseMoveWrapper_) {
Blockly.unbindEvent_(this.onMouseMoveWrapper_);
this.onMouseMoveWrapper_ = null;
}
};
/*
* Handle a mouse-up event while dragging a comment's border or resize handle.
* @param {!Event} e Mouse up event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeMouseUp_ = function(/*e*/) {
Blockly.Touch.clearTouchIdentifier();
this.unbindDragEvents_();
};
/**
* Resize this comment to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeMouseMove_ = function(e) {
this.autoLayout_ = false;
var newXY = this.workspace.moveDrag(e);
this.setSize_(this.RTL ? -newXY.x : newXY.x, newXY.y);
};
/**
* Callback function triggered when the comment has resized.
* Resize the text area accordingly.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.resizeComment_ = function() {
var size = this.getHeightWidth();
var topOffset = Blockly.WorkspaceCommentSvg.TOP_OFFSET;
var textOffset = Blockly.WorkspaceCommentSvg.TEXTAREA_OFFSET * 2;
this.foreignObject_.setAttribute('width',
size.width);
this.foreignObject_.setAttribute('height',
size.height - topOffset);
if (this.RTL) {
this.foreignObject_.setAttribute('x',
-size.width);
}
this.textarea_.style.width =
(size.width - textOffset) + 'px';
this.textarea_.style.height =
(size.height - textOffset - topOffset) + 'px';
};
/**
* Set size
* @param {number} width width of the container
* @param {number} height height of the container
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.setSize_ = function(width, height) {
// Minimum size of a comment.
width = Math.max(width, 45);
height = Math.max(height, 20 + Blockly.WorkspaceCommentSvg.TOP_OFFSET);
this.width_ = width;
this.height_ = height;
this.svgRect_.setAttribute('width', width);
this.svgRect_.setAttribute('height', height);
this.svgRectTarget_.setAttribute('width', width);
this.svgRectTarget_.setAttribute('height', height);
this.svgHandleTarget_.setAttribute('width', width);
this.svgHandleTarget_.setAttribute('height', Blockly.WorkspaceCommentSvg.TOP_OFFSET);
if (this.RTL) {
this.svgRect_.setAttribute('transform', 'scale(-1 1)');
this.svgRectTarget_.setAttribute('transform', 'scale(-1 1)');
}
var resizeSize = Blockly.WorkspaceCommentSvg.RESIZE_SIZE;
if (this.resizeGroup_) {
if (this.RTL) {
// Mirror the resize group.
this.resizeGroup_.setAttribute('transform', 'translate(' +
(-width + resizeSize) + ',' + (height - resizeSize) + ') scale(-1 1)');
this.deleteGroup_.setAttribute('transform', 'translate(' +
(-width + resizeSize) + ',' + (-resizeSize) + ') scale(-1 1)');
} else {
this.resizeGroup_.setAttribute('transform', 'translate(' +
(width - resizeSize) + ',' +
(height - resizeSize) + ')');
this.deleteGroup_.setAttribute('transform', 'translate(' +
(width - resizeSize) + ',' +
(-resizeSize) + ')');
}
}
// Allow the contents to resize.
this.resizeComment_();
};
/**
* Dispose of any rendered comment components.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.disposeInternal_ = function() {
this.textarea_ = null;
this.foreignObject_ = null;
this.svgRectTarget_ = null;
this.svgHandleTarget_ = null;
};
/**
* Set the focus on the text area.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setFocus = function() {
var comment = this;
this.focused_ = true;
// Defer CSS changes.
setTimeout(function() {
comment.textarea_.focus();
comment.addFocus();
Blockly.utils.addClass(
comment.svgRectTarget_, 'blocklyCommentTargetFocused');
Blockly.utils.addClass(
comment.svgHandleTarget_, 'blocklyCommentHandleTargetFocused');
}, 0);
};
/**
* Remove focus from the text area.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.blurFocus = function() {
var comment = this;
this.focused_ = false;
// Defer CSS changes.
setTimeout(function() {
comment.textarea_.blur();
comment.removeFocus();
Blockly.utils.removeClass(
comment.svgRectTarget_, 'blocklyCommentTargetFocused');
Blockly.utils.removeClass(
comment.svgHandleTarget_, 'blocklyCommentHandleTargetFocused');
}, 0);
};

View File

@@ -0,0 +1,591 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2017 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 Object representing a code comment on a rendered workspace.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.WorkspaceCommentSvg');
goog.require('Blockly.Events.CommentCreate');
goog.require('Blockly.Events.CommentDelete');
goog.require('Blockly.Events.CommentMove');
goog.require('Blockly.WorkspaceComment');
/**
* Class for a workspace comment's SVG representation.
* @param {!Blockly.Workspace} workspace The block's workspace.
* @param {string} content The content of this workspace comment.
* @param {number} height Height of the comment.
* @param {number} width Width of the comment.
* @param {string=} opt_id Optional ID. Use this ID if provided, otherwise
* create a new ID.
* @extends {Blockly.WorkspaceComment}
* @constructor
*/
Blockly.WorkspaceCommentSvg = function(workspace, content, height, width,
opt_id) {
// Create core elements for the block.
/**
* @type {SVGElement}
* @private
*/
this.svgGroup_ = Blockly.utils.createSvgElement(
'g', {'class': 'blocklyComment'}, null);
this.svgGroup_.translate_ = '';
this.svgRect_ = Blockly.utils.createSvgElement(
'rect',
{
'class': 'blocklyCommentRect',
'x': 0,
'y': 0,
'rx': Blockly.WorkspaceCommentSvg.BORDER_RADIUS,
'ry': Blockly.WorkspaceCommentSvg.BORDER_RADIUS
});
this.svgGroup_.appendChild(this.svgRect_);
/**
* Whether the comment is rendered onscreen and is a part of the DOM.
* @type {boolean}
* @private
*/
this.rendered_ = false;
/**
* Whether to move the comment to the drag surface when it is dragged.
* True if it should move, false if it should be translated directly.
* @type {boolean}
* @private
*/
this.useDragSurface_ =
Blockly.utils.is3dSupported() && !!workspace.blockDragSurface_;
Blockly.WorkspaceCommentSvg.superClass_.constructor.call(this,
workspace, content, height, width, opt_id);
this.render();
}; goog.inherits(Blockly.WorkspaceCommentSvg, Blockly.WorkspaceComment);
/**
* The width and height to use to size a workspace comment when it is first
* added, before it has been edited by the user.
* @type {number}
* @package
*/
Blockly.WorkspaceCommentSvg.DEFAULT_SIZE = 100;
/**
* Dispose of this comment.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.dispose = function() {
if (!this.workspace) {
// The comment has already been deleted.
return;
}
// If this comment is being dragged, unlink the mouse events.
if (Blockly.selected == this) {
this.unselect();
this.workspace.cancelCurrentGesture();
}
if (Blockly.Events.isEnabled()) {
Blockly.Events.fire(new Blockly.Events.CommentDelete(this));
}
goog.dom.removeNode(this.svgGroup_);
// Sever JavaScript to DOM connections.
this.svgGroup_ = null;
this.svgRect_ = null;
// Dispose of any rendered components
this.disposeInternal_();
Blockly.Events.disable();
Blockly.WorkspaceCommentSvg.superClass_.dispose.call(this);
Blockly.Events.enable();
};
/**
* Create and initialize the SVG representation of a workspace comment.
* May be called more than once.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.initSvg = function() {
goog.asserts.assert(this.workspace.rendered, 'Workspace is headless.');
if (!this.workspace.options.readOnly && !this.eventsInit_) {
Blockly.bindEventWithChecks_(
this.svgRectTarget_, 'mousedown', this, this.pathMouseDown_);
Blockly.bindEventWithChecks_(
this.svgHandleTarget_, 'mousedown', this, this.pathMouseDown_);
}
this.eventsInit_ = true;
this.updateMovable();
if (!this.getSvgRoot().parentNode) {
this.workspace.getBubbleCanvas().appendChild(this.getSvgRoot());
}
};
/**
* Handle a mouse-down on an SVG comment.
* @param {!Event} e Mouse down event or touch start event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.pathMouseDown_ = function(e) {
var gesture = this.workspace.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
}
};
/**
* Show the context menu for this workspace comment.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.showContextMenu_ = function(e) {
if (this.workspace.options.readOnly) {
return;
}
// Save the current workspace comment in a variable for use in closures.
var comment = this;
var menuOptions = [];
if (this.isDeletable() && this.isMovable()) {
menuOptions.push(Blockly.ContextMenu.commentDuplicateOption(comment));
menuOptions.push(Blockly.ContextMenu.commentDeleteOption(comment));
}
Blockly.ContextMenu.show(e, menuOptions, this.RTL);
};
/**
* Select this comment. Highlight it visually.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.select = function() {
if (Blockly.selected == this) {
return;
}
var oldId = null;
if (Blockly.selected) {
oldId = Blockly.selected.id;
// Unselect any previously selected block.
Blockly.Events.disable();
try {
Blockly.selected.unselect();
} finally {
Blockly.Events.enable();
}
}
var event = new Blockly.Events.Ui(null, 'selected', oldId, this.id);
event.workspaceId = this.workspace.id;
Blockly.Events.fire(event);
Blockly.selected = this;
this.addSelect();
};
/**
* Unselect this comment. Remove its highlighting.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.unselect = function() {
if (Blockly.selected != this) {
return;
}
var event = new Blockly.Events.Ui(null, 'selected', this.id, null);
event.workspaceId = this.workspace.id;
Blockly.Events.fire(event);
Blockly.selected = null;
this.removeSelect();
this.blurFocus();
};
/**
* Select this comment. Highlight it visually.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.addSelect = function() {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklySelected');
this.setFocus();
};
/**
* Unselect this comment. Remove its highlighting.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.removeSelect = function() {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklySelected');
this.blurFocus();
};
/**
* Focus this comment. Highlight it visually.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.addFocus = function() {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyFocused');
};
/**
* Unfocus this comment. Remove its highlighting.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.removeFocus = function() {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyFocused');
};
/**
* Return the coordinates of the top-left corner of this comment relative to the
* drawing surface's origin (0,0), in workspace units.
* If the comment is on the workspace, (0, 0) is the origin of the workspace
* coordinate system.
* This does not change with workspace scale.
* @return {!goog.math.Coordinate} Object with .x and .y properties in
* workspace coordinates.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getRelativeToSurfaceXY = function() {
var x = 0;
var y = 0;
var dragSurfaceGroup = this.useDragSurface_ ?
this.workspace.blockDragSurface_.getGroup() : null;
var element = this.getSvgRoot();
if (element) {
do {
// Loop through this comment and every parent.
var xy = Blockly.utils.getRelativeXY(element);
x += xy.x;
y += xy.y;
// If this element is the current element on the drag surface, include
// the translation of the drag surface itself.
if (this.useDragSurface_ &&
this.workspace.blockDragSurface_.getCurrentBlock() == element) {
var surfaceTranslation =
this.workspace.blockDragSurface_.getSurfaceTranslation();
x += surfaceTranslation.x;
y += surfaceTranslation.y;
}
element = element.parentNode;
} while (element && element != this.workspace.getBubbleCanvas() &&
element != dragSurfaceGroup);
}
this.xy_ = new goog.math.Coordinate(x, y);
return this.xy_;
};
/**
* Move a comment by a relative offset.
* @param {number} dx Horizontal offset, in workspace units.
* @param {number} dy Vertical offset, in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.moveBy = function(dx, dy) {
var event = new Blockly.Events.CommentMove(this);
// TODO: Do I need to look up the relative to surface XY position here?
var xy = this.getRelativeToSurfaceXY();
this.translate(xy.x + dx, xy.y + dy);
this.xy_ = new goog.math.Coordinate(xy.x + dx, xy.y + dy);
event.recordNew();
Blockly.Events.fire(event);
this.workspace.resizeContents();
};
/**
* Transforms a comment by setting the translation on the transform attribute
* of the block's SVG.
* @param {number} x The x coordinate of the translation in workspace units.
* @param {number} y The y coordinate of the translation in workspace units.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.translate = function(x, y) {
this.xy_ = new goog.math.Coordinate(x, y);
this.getSvgRoot().setAttribute('transform',
'translate(' + x + ',' + y + ')');
};
/**
* Move this comment to its workspace's drag surface, accounting for positioning.
* Generally should be called at the same time as setDragging(true).
* Does nothing if useDragSurface_ is false.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.moveToDragSurface_ = function() {
if (!this.useDragSurface_) {
return;
}
// The translation for drag surface blocks,
// is equal to the current relative-to-surface position,
// to keep the position in sync as it move on/off the surface.
// This is in workspace coordinates.
var xy = this.getRelativeToSurfaceXY();
this.clearTransformAttributes_();
this.workspace.blockDragSurface_.translateSurface(xy.x, xy.y);
// Execute the move on the top-level SVG component
this.workspace.blockDragSurface_.setBlocksAndShow(this.getSvgRoot());
};
/**
* Move this comment back to the workspace block canvas.
* Generally should be called at the same time as setDragging(false).
* Does nothing if useDragSurface_ is false.
* @param {!goog.math.Coordinate} newXY The position the comment should take on
* on the workspace canvas, in workspace coordinates.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.moveOffDragSurface_ = function(newXY) {
if (!this.useDragSurface_) {
return;
}
// Translate to current position, turning off 3d.
this.translate(newXY.x, newXY.y);
this.workspace.blockDragSurface_.clearAndHide(this.workspace.getCanvas());
};
/**
* Move this comment during a drag, taking into account whether we are using a
* drag surface to translate blocks.
* @param {?Blockly.BlockDragSurfaceSvg} dragSurface The surface that carries
* rendered items during a drag, or null if no drag surface is in use.
* @param {!goog.math.Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.moveDuringDrag = function(dragSurface, newLoc) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
} else {
this.svgGroup_.translate_ = 'translate(' + newLoc.x + ',' + newLoc.y + ')';
this.svgGroup_.setAttribute('transform',
this.svgGroup_.translate_ + this.svgGroup_.skew_);
}
};
/**
* Move the bubble group to the specified location in workspace coordinates.
* @param {number} x The x position to move to.
* @param {number} y The y position to move to.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.moveTo = function(x, y) {
this.translate(x, y);
};
/**
* Clear the comment of transform="..." attributes.
* Used when the comment is switching from 3d to 2d transform or vice versa.
* @private
*/
Blockly.WorkspaceCommentSvg.prototype.clearTransformAttributes_ = function() {
Blockly.utils.removeAttribute(this.getSvgRoot(), 'transform');
};
/**
* Returns the coordinates of a bounding box describing the dimensions of this
* comment.
* Coordinate system: workspace coordinates.
* @return {!{topLeft: goog.math.Coordinate, bottomRight: goog.math.Coordinate}}
* Object with top left and bottom right coordinates of the bounding box.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getBoundingRectangle = function() {
var blockXY = this.getRelativeToSurfaceXY();
var commentBounds = this.getHeightWidth();
var topLeft;
var bottomRight;
if (this.RTL) {
topLeft = new goog.math.Coordinate(blockXY.x - (commentBounds.width),
blockXY.y);
// Add the width of the tab/puzzle piece knob to the x coordinate
// since X is the corner of the rectangle, not the whole puzzle piece.
bottomRight = new goog.math.Coordinate(blockXY.x,
blockXY.y + commentBounds.height);
} else {
// Subtract the width of the tab/puzzle piece knob to the x coordinate
// since X is the corner of the rectangle, not the whole puzzle piece.
topLeft = new goog.math.Coordinate(blockXY.x, blockXY.y);
bottomRight = new goog.math.Coordinate(blockXY.x + commentBounds.width,
blockXY.y + commentBounds.height);
}
return {topLeft: topLeft, bottomRight: bottomRight};
};
/**
* Add or remove the UI indicating if this comment is movable or not.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.updateMovable = function() {
if (this.isMovable()) {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable');
} else {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggable');
}
};
/**
* Set whether this comment is movable or not.
* @param {boolean} movable True if movable.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setMovable = function(movable) {
Blockly.WorkspaceCommentSvg.superClass_.setMovable.call(this, movable);
this.updateMovable();
};
/**
* Recursively adds or removes the dragging class to this node and its children.
* @param {boolean} adding True if adding, false if removing.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setDragging = function(adding) {
if (adding) {
var group = this.getSvgRoot();
group.translate_ = '';
group.skew_ = '';
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDragging');
} else {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDragging');
}
};
/**
* Return the root node of the SVG or null if none exists.
* @return {Element} The root SVG node (probably a group).
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getSvgRoot = function() {
return this.svgGroup_;
};
/**
* Returns this comment's text.
* @return {string} Comment text.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.getContent = function() {
return this.textarea_ ? this.textarea_.value : this.content_;
};
/**
* Set this comment's content.
* @param {string} content Comment content.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setContent = function(content) {
Blockly.WorkspaceCommentSvg.superClass_.setContent.call(this, content);
if (this.textarea_) {
this.textarea_.value = content;
}
};
/**
* Update the cursor over this comment by adding or removing a class.
* @param {boolean} enable True if the delete cursor should be shown, false
* otherwise.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.setDeleteStyle = function(enable) {
if (enable) {
Blockly.utils.addClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggingDelete');
} else {
Blockly.utils.removeClass(
/** @type {!Element} */ (this.svgGroup_), 'blocklyDraggingDelete');
}
};
Blockly.WorkspaceCommentSvg.prototype.setAutoLayout = function() {
// NOP for compatibility with the bubble dragger.
};
/**
* Decode an XML comment tag and create a rendered comment on the workspace.
* @param {!Element} xmlComment XML comment element.
* @param {!Blockly.Workspace} workspace The workspace.
* @param {number=} opt_wsWidth The width of the workspace, which is used to
* position comments correctly in RTL.
* @return {!Blockly.WorkspaceCommentSvg} The created workspace comment.
* @package
*/
Blockly.WorkspaceCommentSvg.fromXml = function(xmlComment, workspace,
opt_wsWidth) {
Blockly.Events.disable();
try {
var info = Blockly.WorkspaceComment.parseAttributes(xmlComment);
var comment = new Blockly.WorkspaceCommentSvg(workspace,
info.content, info.h, info.w, info.id);
if (workspace.rendered) {
comment.initSvg();
comment.render(false);
}
// Position the comment correctly, taking into account the width of a
// rendered RTL workspace.
if (!isNaN(info.x) && !isNaN(info.y)) {
if (workspace.RTL) {
var wsWidth = opt_wsWidth || workspace.getWidth();
comment.moveBy(wsWidth - info.x, info.y);
} else {
comment.moveBy(info.x, info.y);
}
}
} finally {
Blockly.Events.enable();
}
Blockly.WorkspaceComment.fireCreateEvent(comment);
return comment;
};
/**
* Encode a comment subtree as XML with XY coordinates.
* @param {boolean=} opt_noId True if the encoder should skip the comment id.
* @return {!Element} Tree of XML elements.
* @package
*/
Blockly.WorkspaceCommentSvg.prototype.toXmlWithXY = function(opt_noId) {
var width; // Not used in LTR.
if (this.workspace.RTL) {
// Here be performance dragons: This calls getMetrics().
width = this.workspace.getWidth();
}
var element = this.toXml(opt_noId);
var xy = this.getRelativeToSurfaceXY();
element.setAttribute('x',
Math.round(this.workspace.RTL ? width - xy.x : xy.x));
element.setAttribute('y', Math.round(xy.y));
element.setAttribute('h', this.getHeight());
element.setAttribute('w', this.getWidth());
return element;
};

View File

@@ -41,6 +41,8 @@ goog.require('Blockly.Trashcan');
goog.require('Blockly.VariablesDynamic');
goog.require('Blockly.Workspace');
goog.require('Blockly.WorkspaceAudio');
goog.require('Blockly.WorkspaceComment');
goog.require('Blockly.WorkspaceCommentSvg');
goog.require('Blockly.WorkspaceDragSurfaceSvg');
goog.require('Blockly.Xml');
goog.require('Blockly.ZoomControls');
@@ -243,6 +245,14 @@ Blockly.WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false;
*/
Blockly.WorkspaceSvg.prototype.isDragSurfaceActive_ = false;
/**
* The first parent div with 'injectionDiv' in the name, or null if not set.
* Access this with getInjectionDiv.
* @type {!Element}
* @private
*/
Blockly.WorkspaceSvg.prototype.injectionDiv_ = null;
/**
* Last known position of the page scroll.
* This is used to determine whether we have recalculated screen coordinate
@@ -352,6 +362,29 @@ Blockly.WorkspaceSvg.prototype.getOriginOffsetInPixels = function() {
return Blockly.utils.getInjectionDivXY_(this.svgBlockCanvas_);
};
/**
* Return the injection div that is a parent of this workspace.
* Walks the DOM the first time it's called, then returns a cached value.
* @return {!Element} The first parent div with 'injectionDiv' in the name.
* @package
*/
Blockly.WorkspaceSvg.prototype.getInjectionDiv = function() {
// NB: it would be better to pass this in at createDom, but is more likely to
// break existing uses of Blockly.
if (!this.injectionDiv_) {
var element = this.svgGroup_;
while (element) {
var classes = element.getAttribute('class') || '';
if ((' ' + classes + ' ').indexOf(' injectionDiv ') != -1) {
this.injectionDiv_ = element;
break;
}
element = element.parentNode;
}
}
return this.injectionDiv_;
};
/**
* Save resize handler data so we can delete it later in dispose.
* @param {!Array.<!Array>} handler Data that can be passed to unbindEvent_.
@@ -897,6 +930,18 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
if (this.currentGesture_) {
this.currentGesture_.cancel(); // Dragging while pasting? No.
}
if (xmlBlock.tagName.toLowerCase() == 'comment') {
this.pasteWorkspaceComment_(xmlBlock);
} else {
this.pasteBlock_(xmlBlock);
}
};
/**
* Paste the provided block onto the workspace.
* @param {!Element} xmlBlock XML block element.
*/
Blockly.WorkspaceSvg.prototype.pasteBlock_ = function(xmlBlock) {
Blockly.Events.disable();
try {
var block = Blockly.Xml.domToBlock(xmlBlock, this);
@@ -952,6 +997,37 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) {
block.select();
};
/**
* Paste the provided comment onto the workspace.
* @param {!Element} xmlComment XML workspace comment element.
* @private
*/
Blockly.WorkspaceSvg.prototype.pasteWorkspaceComment_ = function(xmlComment) {
Blockly.Events.disable();
try {
var comment = Blockly.WorkspaceCommentSvg.fromXml(xmlComment, this);
// Move the duplicate to original position.
var commentX = parseInt(xmlComment.getAttribute('x'), 10);
var commentY = parseInt(xmlComment.getAttribute('y'), 10);
if (!isNaN(commentX) && !isNaN(commentY)) {
if (this.RTL) {
commentX = -commentX;
}
// Offset workspace comment.
// TODO: #1719 properly offset comment such that it's not interfereing with any blocks
commentX += 50;
commentY += 50;
comment.moveBy(commentX, commentY);
}
} finally {
Blockly.Events.enable();
}
if (Blockly.Events.isEnabled()) {
// TODO: Fire a Workspace Comment Create event
}
comment.select();
};
/**
* Refresh the toolbox unless there's a drag in progress.
* @package
@@ -1126,17 +1202,19 @@ Blockly.WorkspaceSvg.prototype.onMouseWheel_ = function(e) {
*/
Blockly.WorkspaceSvg.prototype.getBlocksBoundingBox = function() {
var topBlocks = this.getTopBlocks(false);
var topComments = this.getTopComments(false);
var topElements = topBlocks.concat(topComments);
// There are no blocks, return empty rectangle.
if (!topBlocks.length) {
if (!topElements.length) {
return {x: 0, y: 0, width: 0, height: 0};
}
// Initialize boundary using the first block.
var boundary = topBlocks[0].getBoundingRectangle();
var boundary = topElements[0].getBoundingRectangle();
// Start at 1 since the 0th block was used for initialization
for (var i = 1; i < topBlocks.length; i++) {
var blockBoundary = topBlocks[i].getBoundingRectangle();
for (var i = 1; i < topElements.length; i++) {
var blockBoundary = topElements[i].getBoundingRectangle();
if (blockBoundary.topLeft.x < boundary.topLeft.x) {
boundary.topLeft.x = blockBoundary.topLeft.x;
}

410
core/ws_comment_events.js Normal file
View File

@@ -0,0 +1,410 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2018 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 Classes for all comment events.
* @author fenichel@google.com (Rachel Fenichel)
*/
'use strict';
goog.provide('Blockly.Events.CommentBase');
goog.provide('Blockly.Events.CommentChange');
goog.provide('Blockly.Events.CommentCreate');
goog.provide('Blockly.Events.CommentDelete');
goog.provide('Blockly.Events.CommentMove');
goog.require('Blockly.Events');
goog.require('Blockly.Events.Abstract');
goog.require('goog.math.Coordinate');
/**
* Abstract class for a comment event.
* @param {Blockly.WorkspaceComment} comment The comment this event corresponds
* to.
* @extends {Blockly.Events.Abstract}
* @constructor
*/
Blockly.Events.CommentBase = function(comment) {
/**
* The ID of the comment this event pertains to.
* @type {string}
*/
this.commentId = comment.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = comment.workspace.id;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = Blockly.Events.group_;
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
this.recordUndo = Blockly.Events.recordUndo;
};
goog.inherits(Blockly.Events.CommentBase, Blockly.Events.Abstract);
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentBase.prototype.toJson = function() {
var json = {
'type': this.type
};
if (this.group) {
json['group'] = this.group;
}
if (this.commentId) {
json['commentId'] = this.commentId;
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentBase.prototype.fromJson = function(json) {
this.commentId = json['commentId'];
this.group = json['group'];
};
/**
* Class for a comment change event.
* @param {Blockly.WorkspaceComment} comment The comment that is being changed.
* Null for a blank event.
* @param {string} oldContents Previous contents of the comment.
* @param {string} newContents New contents of the comment.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentChange = function(comment, oldContents, newContents) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentChange.superClass_.constructor.call(this, comment);
this.oldContents_ = oldContents;
this.newContents_ = newContents;
};
goog.inherits(Blockly.Events.CommentChange, Blockly.Events.CommentBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentChange.prototype.type = Blockly.Events.COMMENT_CHANGE;
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentChange.prototype.toJson = function() {
var json = Blockly.Events.CommentChange.superClass_.toJson.call(this);
json['newContents'] = this.newContents_;
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentChange.prototype.fromJson = function(json) {
Blockly.Events.CommentChange.superClass_.fromJson.call(this, json);
this.newContents_ = json['newValue'];
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
Blockly.Events.CommentChange.prototype.isNull = function() {
return this.oldContents_ == this.newContents_;
};
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentChange.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
var comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t change non-existent comment: ' + this.commentId);
return;
}
var contents = forward ? this.newContents_ : this.oldContents_;
comment.setContent(contents);
};
/**
* Class for a comment creation event.
* @param {Blockly.WorkspaceComment} comment The created comment.
* Null for a blank event.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentCreate = function(comment) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentCreate.superClass_.constructor.call(this, comment);
this.xml = comment.toXmlWithXY();
};
goog.inherits(Blockly.Events.CommentCreate, Blockly.Events.CommentBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentCreate.prototype.type = Blockly.Events.COMMENT_CREATE;
/**
* Encode the event as JSON.
* TODO (#1266): "Full" and "minimal" serialization.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentCreate.prototype.toJson = function() {
var json = Blockly.Events.CommentCreate.superClass_.toJson.call(this);
json['xml'] = Blockly.Xml.domToText(this.xml);
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentCreate.prototype.fromJson = function(json) {
Blockly.Events.CommentCreate.superClass_.fromJson.call(this, json);
this.xml = Blockly.Xml.textToDom('<xml>' + json['xml'] + '</xml>').firstChild;
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentCreate.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
var xml = goog.dom.createDom('xml');
xml.appendChild(this.xml);
Blockly.Xml.domToWorkspace(xml, workspace);
} else {
var comment = workspace.getCommentById(this.commentId);
if (comment) {
comment.dispose(false, false);
} else {
// Only complain about root-level block.
console.warn("Can't uncreate non-existent comment: " + this.commentId);
}
}
};
/**
* Class for a comment deletion event.
* @param {Blockly.WorkspaceComment} comment The deleted comment.
* Null for a blank event.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentDelete = function(comment) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentDelete.superClass_.constructor.call(this, comment);
this.xml = comment.toXmlWithXY();
};
goog.inherits(Blockly.Events.CommentDelete, Blockly.Events.CommentBase);
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentDelete.prototype.type = Blockly.Events.COMMENT_DELETE;
/**
* Encode the event as JSON.
* TODO (#1266): "Full" and "minimal" serialization.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentDelete.prototype.toJson = function() {
var json = Blockly.Events.CommentDelete.superClass_.toJson.call(this);
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentDelete.prototype.fromJson = function(json) {
Blockly.Events.CommentDelete.superClass_.fromJson.call(this, json);
};
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentDelete.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
if (forward) {
var comment = workspace.getCommentById(this.commentId);
if (comment) {
comment.dispose(false, false);
} else {
// Only complain about root-level block.
console.warn("Can't uncreate non-existent comment: " + this.commentId);
}
} else {
var xml = goog.dom.createDom('xml');
xml.appendChild(this.xml);
Blockly.Xml.domToWorkspace(xml, workspace);
}
};
/**
* Class for a comment move event. Created before the move.
* @param {Blockly.WorkspaceComment} comment The comment that is being moved.
* Null for a blank event.
* @extends {Blockly.Events.CommentBase}
* @constructor
*/
Blockly.Events.CommentMove = function(comment) {
if (!comment) {
return; // Blank event to be populated by fromJson.
}
Blockly.Events.CommentMove.superClass_.constructor.call(this, comment);
/**
* The comment that is being moved. Will be cleared after recording the new
* location.
* @type {?Blockly.WorkspaceComment}
*/
this.comment_ = comment;
/**
* The location before the move, in workspace coordinates.
* @type {!goog.math.Coordinate}
*/
this.oldCoordinate_ = comment.getXY();
/**
* The location after the move, in workspace coordinates.
* @type {!goog.math.Coordinate}
*/
this.newCoordinate_ = null;
};
goog.inherits(Blockly.Events.CommentMove, Blockly.Events.CommentBase);
/**
* Record the comment's new location. Called after the move. Can only be
* called once.
*/
Blockly.Events.CommentMove.prototype.recordNew = function() {
if (!this.comment_) {
throw new Error('Tried to record the new position of a comment on the ' +
'same event twice.');
}
this.newCoordinate_ = this.comment_.getXY();
this.comment_ = null;
};
/**
* Type of this event.
* @type {string}
*/
Blockly.Events.CommentMove.prototype.type = Blockly.Events.COMMENT_MOVE;
/**
* Override the location before the move. Use this if you don't create the
* event until the end of the move, but you know the original location.
* @param {!goog.math.Coordinate} xy The location before the move, in workspace
* coordinates.
*/
Blockly.Events.CommentMove.prototype.setOldCoordinate = function(xy) {
this.oldCoordinate_ = xy;
};
/**
* Encode the event as JSON.
* TODO (#1266): "Full" and "minimal" serialization.
* @return {!Object} JSON representation.
*/
Blockly.Events.CommentMove.prototype.toJson = function() {
var json = Blockly.Events.CommentMove.superClass_.toJson.call(this);
if (this.newCoordinate_) {
json['newCoordinate'] = Math.round(this.newCoordinate_.x) + ',' +
Math.round(this.newCoordinate_.y);
}
return json;
};
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
*/
Blockly.Events.CommentMove.prototype.fromJson = function(json) {
Blockly.Events.CommentMove.superClass_.fromJson.call(this, json);
if (json['newCoordinate']) {
var xy = json['newCoordinate'].split(',');
this.newCoordinate_ =
new goog.math.Coordinate(parseFloat(xy[0]), parseFloat(xy[1]));
}
};
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
*/
Blockly.Events.CommentMove.prototype.isNull = function() {
return goog.math.Coordinate.equals(this.oldCoordinate_, this.newCoordinate_);
};
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
*/
Blockly.Events.CommentMove.prototype.run = function(forward) {
var workspace = this.getEventWorkspace_();
var comment = workspace.getCommentById(this.commentId);
if (!comment) {
console.warn('Can\'t move non-existent comment: ' + this.commentId);
return;
}
var target = forward ? this.newCoordinate_ : this.oldCoordinate_;
// TODO: Check if the comment is being dragged, and give up if so.
var current = comment.getXY();
comment.moveBy(target.x - current.x, target.y - current.y);
};

View File

@@ -45,9 +45,11 @@ goog.require('goog.dom');
*/
Blockly.Xml.workspaceToDom = function(workspace, opt_noId) {
var xml = goog.dom.createDom('xml');
var variables = Blockly.Variables.allUsedVarModels(workspace);
if (variables.length) {
xml.appendChild(Blockly.Xml.variablesToDom(variables));
xml.appendChild(Blockly.Xml.variablesToDom(
Blockly.Variables.allUsedVarModels(workspace)));
var comments = workspace.getTopComments(true);
for (var i = 0, comment; comment = comments[i]; i++) {
xml.appendChild(comment.toXmlWithXY(opt_noId));
}
var blocks = workspace.getTopBlocks(true);
for (var i = 0, block; block = blocks[i]; i++) {
@@ -376,6 +378,7 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) {
console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' +
'swap the arguments.');
}
var width; // Not used in LTR.
if (workspace.RTL) {
width = workspace.getWidth();
@@ -418,6 +421,12 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) {
} else if (name == 'shadow') {
goog.asserts.fail('Shadow block cannot be a top-level block.');
variablesFirst = false;
} else if (name == 'comment') {
if (workspace.rendered) {
Blockly.WorkspaceCommentSvg.fromXml(xmlChild, workspace, width);
} else {
Blockly.WorkspaceComment.fromXml(xmlChild, workspace);
}
} else if (name == 'variables') {
if (variablesFirst) {
Blockly.Xml.domToVariables(xmlChild, workspace);
@@ -562,6 +571,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
return topBlock;
};
/**
* Decode an XML list of variables and add the variables to the workspace.
* @param {!Element} xmlVariables List of XML variable elements.