Simplify Closure-sourced code for menus (#3880)

* Remove cargo-culted bloat from CSS

The `goog-menuitem-icon` and `goog-menuitem-noicon` classes are not present in Blockly.  Blockly doesn’t support the CSS compiler, so #noflip has no effect.  Shorten uncompressible warning string.

Also remove the “Copied from Closure” notes.  These were intended so that the CSS could be easily updated as the Closure Library evolved.  We are no longer linked to the Closure Library.

* Fix bug (in prod) where menu highlighting is lost

Previously, open playground.  Right-click on workspace.  Mouse-over “Add comment” (it highlights).  Mouse over “Download screenshot” (disabled option).  Mouse over “Add comment” (highlighting is lost).

Also remove `canHighlightItem` helper function.  In theory this helps abstract the concept of non-highlightable options.  But in practice it was only called in one of the several places that it should have been.  This was a false abstraction.

* Add support for Space/PgUp/PgDn/Home/End to menus

* Eliminate calls to clearHighlighted

The JSDoc for `setHighlightedIndex` specifically states, “If another item was previously highlighted, it is un-highlighted.”  This is not what was implemented, but it should be.  This commit adds the un-highlighting, and removes all the calls previously required to correct this bug.

* Stop wrapping at top or bottom of menu.

Real OS menus don’t wrap when one cursors off the top or bottom.

Also, replace the overly complicated helper function with a simple 1/-1 step value.

* Remove unused menu code

* Simplify menu roles

Remove unneeded sets to RTL on Menu (only MenuItem cares).

* Fix lack of disposal for context menus.

Context menus only disposed properly when an option was clicked.  If they were dismissed by clicking outside the menu there was no disposal.  This might result in a memory leak.
Also un-extract (inject?) several now trivial functions.

* Remove Component dependency from Menu & MenuItem

Component is now only used by the category tree.

* Remove unused functions in Component

These were used by Menu/MenuItem.

* Fix dependencies.

* Record highlighted menu item by object, not index

Less code, simpler.

* Rename CSS classes goog-menu* to blocklyMenu*

Old classes remain in DOM and are deprecated so that any custom CSS will continue to function.

* Remove unused focus tracker in tree.

* Add support for space/enter to toggle tree cats

* Delete unsettable .isUserCollapsible_ from tree

* Change visibility tags throughout menus.

The previous tags were inherited from Closure and don’t reflect current usage in the Blockly codebase.

The core/components/tree files are non-compliant in this regard, but I’m not going to update them since they need to be replaced and there’s no need to create an interim API change.

* Remove property on DOM element linking to JS obj

Performance is slower (O(n) rather than (O(1)), but ’n’ is the number of entries on the menu, so shouldn’t be more than a dozen or so.

* Fixes a compile error (node != element)

Usually we avoid parentElement in Blockly.  That’s because it has very spotty behaviour with SVG.  But in this case we are in pure HTML.
This commit is contained in:
Neil Fraser
2020-05-06 23:55:17 -04:00
committed by GitHub
parent abd6a53ac2
commit a65afdc189
23 changed files with 511 additions and 848 deletions

View File

@@ -220,7 +220,7 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) {
this.dragBlock(e, currentDragDeltaXY);
this.dragIconData_ = [];
this.fireDragEndEvent_();
Blockly.utils.dom.stopTextWidthCache();
Blockly.blockAnimations.disconnectUiStop();

View File

@@ -115,7 +115,12 @@ Blockly.Component.Error = {
* Error when an attempt is made to add a child component at an out-of-bounds
* index. We don't support sparse child arrays.
*/
CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds'
CHILD_INDEX_OUT_OF_BOUNDS: 'Child component index out of bounds',
/**
* Error when calling an abstract method that should be overriden.
*/
ABSTRACT_METHOD: 'Unimplemented abstract method'
};
/**
@@ -195,12 +200,11 @@ Blockly.Component.prototype.isInDocument = function() {
};
/**
* Creates the initial DOM representation for the component. The default
* implementation is to set this.element_ = div.
* Creates the initial DOM representation for the component.
* @protected
*/
Blockly.Component.prototype.createDom = function() {
this.element_ = document.createElement('div');
throw Error(Blockly.Component.Error.ABSTRACT_METHOD);
};
/**
@@ -223,19 +227,6 @@ Blockly.Component.prototype.render = function(opt_parentElement) {
this.render_(opt_parentElement);
};
/**
* Renders the component before another element. The other element should be in
* the document already.
*
* Throws an Error if the component is already rendered.
*
* @param {Node} sibling Node to render the component before.
* @protected
*/
Blockly.Component.prototype.renderBefore = function(sibling) {
this.render_(/** @type {Element} */ (sibling.parentNode), sibling);
};
/**
* Renders the component. If a parent element is supplied, the component's
* element will be appended to it. If there is no optional parent element and
@@ -497,21 +488,6 @@ Blockly.Component.prototype.getContentElement = function() {
return this.element_;
};
/**
* Set is right-to-left. This function should be used if the component needs
* to know the rendering direction during DOM creation (i.e. before
* {@link #enterDocument} is called and is right-to-left is set).
* @param {boolean} rightToLeft Whether the component is rendered
* right-to-left.
* @package
*/
Blockly.Component.prototype.setRightToLeft = function(rightToLeft) {
if (this.inDocument_) {
throw Error(Blockly.Component.Error.ALREADY_RENDERED);
}
this.rightToLeft_ = rightToLeft;
};
/**
* Returns true if the component has children.
* @return {boolean} True if the component has children.
@@ -569,14 +545,3 @@ Blockly.Component.prototype.forEachChild = function(f, opt_obj) {
f.call(/** @type {?} */ (opt_obj), this.children_[i], i);
}
};
/**
* Returns the 0-based index of the given child component, or -1 if no such
* child is found.
* @param {?Blockly.Component} child The child component.
* @return {number} 0-based index of the child component; -1 if not found.
* @protected
*/
Blockly.Component.prototype.indexOfChild = function(child) {
return this.children_.indexOf(child);
};

View File

@@ -12,20 +12,26 @@
goog.provide('Blockly.Menu');
goog.require('Blockly.Component');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.Coordinate');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.KeyCodes');
goog.require('Blockly.utils.style');
/**
* A basic menu class.
* @constructor
* @extends {Blockly.Component}
*/
Blockly.Menu = function() {
Blockly.Component.call(this);
/**
* Array of menu items.
* (Nulls are never in the array, but typing the array as nullable prevents
* the compiler from objecting to .indexOf(null))
* @type {!Array.<Blockly.MenuItem>}
* @private
*/
this.menuItems_ = [];
/**
* Coordinates of the mousedown event that caused this menu to open. Used to
@@ -38,11 +44,11 @@ Blockly.Menu = function() {
/**
* This is the element that we will listen to the real focus events on.
* A value of -1 means no menuitem is highlighted.
* @type {number}
* A value of null means no menu item is highlighted.
* @type {Blockly.MenuItem}
* @private
*/
this.highlightedIndex_ = -1;
this.highlightedItem_ = null;
/**
* Mouse over event data.
@@ -77,27 +83,73 @@ Blockly.Menu = function() {
* @type {?Blockly.EventData}
* @private
*/
this.onKeyDownWrapper_ = null;
this.onKeyDownHandler_ = null;
/**
* The menu's root DOM element.
* @type {Element}
* @private
*/
this.element_ = null;
/**
* ARIA name for this menu.
* @type {?Blockly.utils.aria.Role}
* @private
*/
this.roleName_ = null;
};
Blockly.utils.object.inherits(Blockly.Menu, Blockly.Component);
/**
* Creates the menu DOM.
* @override
* Add a new menu item to the bottom of this menu.
* @param {!Blockly.MenuItem} menuItem Menu item to append.
*/
Blockly.Menu.prototype.createDom = function() {
Blockly.Menu.prototype.addChild = function(menuItem) {
this.menuItems_.push(menuItem);
};
/**
* Creates the menu DOM.
* @param {!Element} container Element upon which to append this menu.
*/
Blockly.Menu.prototype.render = function(container) {
var element = document.createElement('div');
element.id = this.getId();
this.setElementInternal(element);
// Set class
element.className = 'goog-menu goog-menu-vertical blocklyNonSelectable';
// goog-menu is deprecated, use blocklyMenu. May 2020.
element.className = 'blocklyMenu goog-menu blocklyNonSelectable';
element.tabIndex = 0;
if (this.roleName_) {
Blockly.utils.aria.setRole(element, this.roleName_);
}
this.element_ = element;
// Initialize ARIA role.
Blockly.utils.aria.setRole(element,
this.roleName_ || Blockly.utils.aria.Role.MENU);
// Add menu items.
for (var i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
element.appendChild(menuItem.createDom());
}
// Add event handlers.
this.mouseOverHandler_ = Blockly.bindEventWithChecks_(element,
'mouseover', this, this.handleMouseOver_, true);
this.clickHandler_ = Blockly.bindEventWithChecks_(element,
'click', this, this.handleClick_, true);
this.mouseEnterHandler_ = Blockly.bindEventWithChecks_(element,
'mouseenter', this, this.handleMouseEnter_, true);
this.mouseLeaveHandler_ = Blockly.bindEventWithChecks_(element,
'mouseleave', this, this.handleMouseLeave_, true);
this.onKeyDownHandler_ = Blockly.bindEventWithChecks_(element,
'keydown', this, this.handleKeyEvent_);
container.appendChild(element);
};
/**
* Gets the menu's element.
* @return {Element} The DOM element.
* @package
*/
Blockly.Menu.prototype.getElement = function() {
return this.element_;
};
/**
@@ -108,19 +160,19 @@ Blockly.Menu.prototype.focus = function() {
var el = this.getElement();
if (el) {
el.focus({preventScroll:true});
Blockly.utils.dom.addClass(el, 'focused');
Blockly.utils.dom.addClass(el, 'blocklyFocused');
}
};
/**
* Blur the menu element.
* @package
* @private
*/
Blockly.Menu.prototype.blur = function() {
Blockly.Menu.prototype.blur_ = function() {
var el = this.getElement();
if (el) {
el.blur();
Blockly.utils.dom.removeClass(el, 'focused');
Blockly.utils.dom.removeClass(el, 'blocklyFocused');
}
};
@@ -133,63 +185,11 @@ Blockly.Menu.prototype.setRole = function(roleName) {
this.roleName_ = roleName;
};
/** @override */
Blockly.Menu.prototype.enterDocument = function() {
Blockly.Menu.superClass_.enterDocument.call(this);
this.forEachChild(function(child) {
if (child.isInDocument()) {
this.registerChildId_(child);
}
}, this);
this.attachEvents_();
};
/**
* Cleans up the container before its DOM is removed from the document, and
* removes event handlers. Overrides {@link Blockly.Component#exitDocument}.
* @override
* Dispose of this menu.
*/
Blockly.Menu.prototype.exitDocument = function() {
// {@link #setHighlightedIndex} has to be called before
// {@link Blockly.Component#exitDocument}, otherwise it has no effect.
this.setHighlightedIndex(-1);
Blockly.Menu.superClass_.exitDocument.call(this);
};
/** @override */
Blockly.Menu.prototype.disposeInternal = function() {
Blockly.Menu.superClass_.disposeInternal.call(this);
this.detachEvents_();
};
/**
* Adds the event listeners to the menu.
* @private
*/
Blockly.Menu.prototype.attachEvents_ = function() {
var el = /** @type {!EventTarget} */ (this.getElement());
this.mouseOverHandler_ = Blockly.bindEventWithChecks_(el,
'mouseover', this, this.handleMouseOver_, true);
this.clickHandler_ = Blockly.bindEventWithChecks_(el,
'click', this, this.handleClick_, true);
this.mouseEnterHandler_ = Blockly.bindEventWithChecks_(el,
'mouseenter', this, this.handleMouseEnter_, true);
this.mouseLeaveHandler_ = Blockly.bindEventWithChecks_(el,
'mouseleave', this, this.handleMouseLeave_, true);
this.onKeyDownWrapper_ = Blockly.bindEventWithChecks_(el,
'keydown', this, this.handleKeyEvent);
};
/**
* Removes the event listeners from the menu.
* @private
*/
Blockly.Menu.prototype.detachEvents_ = function() {
Blockly.Menu.prototype.dispose = function() {
// Remove event handlers.
if (this.mouseOverHandler_) {
Blockly.unbindEvent_(this.mouseOverHandler_);
this.mouseOverHandler_ = null;
@@ -206,63 +206,44 @@ Blockly.Menu.prototype.detachEvents_ = function() {
Blockly.unbindEvent_(this.mouseLeaveHandler_);
this.mouseLeaveHandler_ = null;
}
if (this.onKeyDownWrapper_) {
Blockly.unbindEvent_(this.onKeyDownWrapper_);
this.onKeyDownWrapper_ = null;
if (this.onKeyDownHandler_) {
Blockly.unbindEvent_(this.onKeyDownHandler_);
this.onKeyDownHandler_ = null;
}
// Remove menu items.
for (var i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
menuItem.dispose();
}
this.menuItems_length = 0;
this.element_ = null;
};
// Child component management.
/**
* Map of DOM IDs to child menuitems. Each key is the DOM ID of a child
* menuitems's root element; each value is a reference to the child menu
* item itself.
* @type {?Object}
* Returns the child menu item that owns the given DOM element,
* or null if no such menu item is found.
* @param {Element} elem DOM element whose owner is to be returned.
* @return {?Blockly.MenuItem} Menu item for which the DOM element belongs to.
* @private
*/
Blockly.Menu.prototype.childElementIdMap_ = null;
/**
* Creates a DOM ID for the child menuitem and registers it to an internal
* hash table to be able to find it fast by id.
* @param {Blockly.Component} child The child menuitem. Its root element has
* to be created yet.
* @private
*/
Blockly.Menu.prototype.registerChildId_ = function(child) {
// Map the DOM ID of the menuitem's root element to the menuitem itself.
var childElem = child.getElement();
// If the menuitem's root element doesn't have a DOM ID assign one.
var id = childElem.id || (childElem.id = child.getId());
// Lazily create the child element ID map on first use.
if (!this.childElementIdMap_) {
this.childElementIdMap_ = {};
}
this.childElementIdMap_[id] = child;
};
/**
* Returns the child menuitem that owns the given DOM node, or null if no such
* menuitem is found.
* @param {Node} node DOM node whose owner is to be returned.
* @return {?Blockly.MenuItem} menuitem for which the DOM node belongs to.
* @protected
*/
Blockly.Menu.prototype.getMenuItem = function(node) {
// Ensure that this menu actually has child menuitems before
// looking up the menuitem.
if (this.childElementIdMap_) {
var elem = this.getElement();
while (node && node !== elem) {
var id = node.id;
if (id in this.childElementIdMap_) {
return this.childElementIdMap_[id];
Blockly.Menu.prototype.getMenuItem_ = function(elem) {
var menuElem = this.getElement();
// Node might be the menu border (resulting in no associated menu item), or
// a menu item's div, or some element within the menu item.
// Walk up parents until one meets either the menu's root element, or
// a menu item's div.
while (elem && elem != menuElem) {
if (Blockly.utils.dom.hasClass(elem, 'blocklyMenuItem')) {
// Having found a menu item's div, locate that menu item in this menu.
for (var i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
if (menuItem.getElement() == elem) {
return menuItem;
}
}
node = node.parentNode;
}
elem = elem.parentElement;
}
return null;
};
@@ -270,80 +251,35 @@ Blockly.Menu.prototype.getMenuItem = function(node) {
// Highlight management.
/**
* Unhighlight the current highlighted item.
* @protected
* Highlights the given menu item, or clears highlighting if null.
* @param {Blockly.MenuItem} item Item to highlight, or null.
* @private
*/
Blockly.Menu.prototype.unhighlightCurrent = function() {
var highlighted = this.getHighlighted();
if (highlighted) {
highlighted.setHighlighted(false);
Blockly.Menu.prototype.setHighlighted_ = function(item) {
var currentHighlighted = this.highlightedItem_;
if (currentHighlighted) {
currentHighlighted.setHighlighted(false);
this.highlightedItem_ = null;
}
};
/**
* Clears the currently highlighted item.
* @protected
*/
Blockly.Menu.prototype.clearHighlighted = function() {
this.unhighlightCurrent();
this.setHighlightedIndex(-1);
};
/**
* Returns the currently highlighted item (if any).
* @return {?Blockly.Component} Highlighted item (null if none).
* @protected
*/
Blockly.Menu.prototype.getHighlighted = function() {
return this.getChildAt(this.highlightedIndex_);
};
/**
* Highlights the item at the given 0-based index (if any). If another item
* was previously highlighted, it is un-highlighted.
* @param {number} index Index of item to highlight (-1 removes the current
* highlight).
* @protected
*/
Blockly.Menu.prototype.setHighlightedIndex = function(index) {
var child = this.getChildAt(index);
if (child) {
child.setHighlighted(true);
this.highlightedIndex_ = index;
} else if (this.highlightedIndex_ > -1) {
this.getHighlighted().setHighlighted(false);
this.highlightedIndex_ = -1;
}
// Bring the highlighted item into view. This has no effect if the menu is not
// scrollable.
if (child) {
if (item) {
item.setHighlighted(true);
this.highlightedItem_ = item;
// Bring the highlighted item into view. This has no effect if the menu is
// not scrollable.
Blockly.utils.style.scrollIntoContainerView(
/** @type {!Element} */ (child.getElement()),
/** @type {!Element} */ (item.getElement()),
/** @type {!Element} */ (this.getElement()));
}
};
/**
* Highlights the given item if it exists and is a child of the container;
* otherwise un-highlights the currently highlighted item.
* @param {Blockly.MenuItem} item Item to highlight.
* @protected
*/
Blockly.Menu.prototype.setHighlighted = function(item) {
this.setHighlightedIndex(this.indexOfChild(item));
};
/**
* Highlights the next highlightable item (or the first if nothing is currently
* highlighted).
* @package
*/
Blockly.Menu.prototype.highlightNext = function() {
this.unhighlightCurrent();
this.highlightHelper(function(index, max) {
return (index + 1) % max;
}, this.highlightedIndex_);
var index = this.menuItems_.indexOf(this.highlightedItem_);
this.highlightHelper_(index, 1);
};
/**
@@ -352,91 +288,76 @@ Blockly.Menu.prototype.highlightNext = function() {
* @package
*/
Blockly.Menu.prototype.highlightPrevious = function() {
this.unhighlightCurrent();
this.highlightHelper(function(index, max) {
index--;
return index < 0 ? max - 1 : index;
}, this.highlightedIndex_);
var index = this.menuItems_.indexOf(this.highlightedItem_);
this.highlightHelper_(index < 0 ? this.menuItems_.length : index, -1);
};
/**
* Highlights the first highlightable item.
* @private
*/
Blockly.Menu.prototype.highlightFirst_ = function() {
this.highlightHelper_(-1, 1);
};
/**
* Highlights the last highlightable item.
* @private
*/
Blockly.Menu.prototype.highlightLast_ = function() {
this.highlightHelper_(this.menuItems_.length, -1);
};
/**
* Helper function that manages the details of moving the highlight among
* child menuitems in response to keyboard events.
* @param {function(this: Blockly.Component, number, number) : number} fn
* Function that accepts the current and maximum indices, and returns the
* next index to check.
* @param {number} startIndex Start index.
* @return {boolean} Whether the highlight has changed.
* @protected
* @param {number} delta Step direction: 1 to go down, -1 to go up.
* @private
*/
Blockly.Menu.prototype.highlightHelper = function(fn, startIndex) {
// If the start index is -1 (meaning there's nothing currently highlighted),
// try starting from the currently open item, if any.
var curIndex =
startIndex < 0 ? -1 : startIndex;
var numItems = this.getChildCount();
curIndex = fn.call(this, curIndex, numItems);
var visited = 0;
while (visited <= numItems) {
var menuItem = /** @type {Blockly.MenuItem} */ (this.getChildAt(curIndex));
if (menuItem && this.canHighlightItem(menuItem)) {
this.setHighlightedIndex(curIndex);
return true;
Blockly.Menu.prototype.highlightHelper_ = function(startIndex, delta) {
var index = startIndex + delta;
var menuItem;
while ((menuItem = this.menuItems_[index])) {
if (menuItem.isEnabled()) {
this.setHighlighted_(menuItem);
break;
}
visited++;
curIndex = fn.call(this, curIndex, numItems);
index += delta;
}
return false;
};
/**
* Returns whether the given item can be highlighted.
* @param {Blockly.MenuItem} item The item to check.
* @return {boolean} Whether the item can be highlighted.
* @protected
*/
Blockly.Menu.prototype.canHighlightItem = function(item) {
return item.isEnabled();
};
// Mouse events.
/**
* Handles mouseover events. Highlight menuitems as the user
* hovers over them.
* @param {Event} e Mouse event to handle.
* Handles mouseover events. Highlight menuitems as the user hovers over them.
* @param {!Event} e Mouse event to handle.
* @private
*/
Blockly.Menu.prototype.handleMouseOver_ = function(e) {
var menuItem = this.getMenuItem(/** @type {Node} */ (e.target));
var menuItem = this.getMenuItem_(/** @type {Element} */ (e.target));
if (menuItem) {
if (menuItem.isEnabled()) {
var currentHighlighted = this.getHighlighted();
if (currentHighlighted === menuItem) {
return;
if (this.highlightedItem_ != menuItem) {
this.setHighlighted_(menuItem);
}
this.unhighlightCurrent();
this.setHighlighted(menuItem);
} else {
this.unhighlightCurrent();
this.setHighlighted_(null);
}
}
};
/**
* Handles click events. Pass the event onto the child
* menuitem to handle.
* @param {Event} e Click to handle.
* Handles click events. Pass the event onto the child menuitem to handle.
* @param {!Event} e Click event to handle.
* @private
*/
Blockly.Menu.prototype.handleClick_ = function(e) {
var oldCoords = this.openingCoords;
// Clear out the saved opening coords immediately so they're not used twice.
this.openingCoords = null;
if (oldCoords && typeof e.clientX === 'number') {
if (oldCoords && typeof e.clientX == 'number') {
var newCoords = new Blockly.utils.Coordinate(e.clientX, e.clientY);
if (Blockly.utils.Coordinate.distance(oldCoords, newCoords) < 1) {
// This menu was opened by a mousedown and we're handling the consequent
@@ -447,10 +368,9 @@ Blockly.Menu.prototype.handleClick_ = function(e) {
}
}
var menuItem = this.getMenuItem(/** @type {Node} */ (e.target));
if (menuItem && menuItem.handleClick(e)) {
e.preventDefault();
var menuItem = this.getMenuItem_(/** @type {Element} */ (e.target));
if (menuItem) {
menuItem.performAction();
}
};
@@ -470,60 +390,35 @@ Blockly.Menu.prototype.handleMouseEnter_ = function(_e) {
*/
Blockly.Menu.prototype.handleMouseLeave_ = function(_e) {
if (this.getElement()) {
this.blur();
this.clearHighlighted();
this.blur_();
this.setHighlighted_(null);
}
};
// Keyboard events.
/**
* Attempts to handle a keyboard event, if the menuitem is enabled, by calling
* {@link handleKeyEventInternal}. Considered protected; should only be used
* within this package and by subclasses.
* @param {Event} e Key event to handle.
* @return {boolean} Whether the key event was handled.
* @protected
* Attempts to handle a keyboard event, if the menu item is enabled, by calling
* {@link handleKeyEventInternal_}.
* @param {!Event} e Key event to handle.
* @private
*/
Blockly.Menu.prototype.handleKeyEvent = function(e) {
if (this.getChildCount() != 0 &&
this.handleKeyEventInternal(e)) {
e.preventDefault();
e.stopPropagation();
return true;
Blockly.Menu.prototype.handleKeyEvent_ = function(e) {
if (!this.menuItems_.length) {
// Empty menu.
return;
}
return false;
};
/**
* Attempts to handle a keyboard event; returns true if the event was handled,
* false otherwise. If the container is enabled, and a child is highlighted,
* calls the child menuitem's `handleKeyEvent` method to give the menuitem
* a chance to handle the event first.
* @param {Event} e Key event to handle.
* @return {boolean} Whether the event was handled by the container (or one of
* its children).
* @protected
*/
Blockly.Menu.prototype.handleKeyEventInternal = function(e) {
// Give the highlighted menuitem the chance to handle the key event.
var highlighted = this.getHighlighted();
if (highlighted && typeof highlighted.handleKeyEvent == 'function' &&
highlighted.handleKeyEvent(e)) {
return true;
}
// Do not handle the key event if any modifier key is pressed.
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
return false;
// Do not handle the key event if any modifier key is pressed.
return;
}
// Either nothing is highlighted, or the highlighted menuitem didn't handle
// the key event, so attempt to handle it here.
var highlighted = this.highlightedItem_;
switch (e.keyCode) {
case Blockly.utils.KeyCodes.ENTER:
case Blockly.utils.KeyCodes.SPACE:
if (highlighted) {
highlighted.performActionInternal(e);
highlighted.performAction();
}
break;
@@ -535,9 +430,21 @@ Blockly.Menu.prototype.handleKeyEventInternal = function(e) {
this.highlightNext();
break;
default:
return false;
}
case Blockly.utils.KeyCodes.PAGE_UP:
case Blockly.utils.KeyCodes.HOME:
this.highlightFirst_();
break;
return true;
case Blockly.utils.KeyCodes.PAGE_DOWN:
case Blockly.utils.KeyCodes.END:
this.highlightLast_();
break;
default:
// Not a key the menu is interested in.
return;
}
// The menu used this key, don't let it have secondary effects.
e.preventDefault();
e.stopPropagation();
};

View File

@@ -12,10 +12,9 @@
goog.provide('Blockly.MenuItem');
goog.require('Blockly.Component');
goog.require('Blockly.utils.aria');
goog.require('Blockly.utils.dom');
goog.require('Blockly.utils.object');
goog.require('Blockly.utils.IdGenerator');
/**
@@ -25,121 +24,134 @@ goog.require('Blockly.utils.object');
* the item.
* @param {string=} opt_value Data/model associated with the menu item.
* @constructor
* @extends {Blockly.Component}
*/
Blockly.MenuItem = function(content, opt_value) {
Blockly.Component.call(this);
this.setContentInternal(content);
this.setValue(opt_value);
/**
* Human-readable text of this menu item.
* @type {string}
* @private
*/
this.content_ = content;
/**
* Machine-readable value of this menu item.
* @type {string|undefined}
* @private
*/
this.value_ = opt_value;
/**
* Is the menu item clickable, as opposed to greyed-out.
* @type {boolean}
* @private
*/
this.enabled_ = true;
/**
* @type {Blockly.MenuItem}
* The DOM element for the menu item.
* @type {?Element}
* @private
*/
this.previousSibling_;
this.element_ = null;
/**
* @type {Blockly.MenuItem}
* Whether the menu item is rendered right-to-left.
* @type {boolean}
* @private
*/
this.nextSibling_;
this.rightToLeft_ = false;
/**
* ARIA name for this menu.
* @type {?Blockly.utils.aria.Role}
* @private
*/
this.roleName_ = null;
/**
* Is this menu item checkable.
* @type {boolean}
* @private
*/
this.checkable_ = false;
/**
* Is this menu item currently checked.
* @type {boolean}
* @private
*/
this.checked_ = false;
/**
* Bound function to call when this menu item is clicked.
* @type {Function}
* @private
*/
this.actionHandler_ = null;
};
Blockly.utils.object.inherits(Blockly.MenuItem, Blockly.Component);
/**
* Creates the menuitem's DOM.
* @override
* @return {!Element} Completed DOM.
*/
Blockly.MenuItem.prototype.createDom = function() {
var element = document.createElement('div');
element.id = this.getId();
this.setElementInternal(element);
element.id = Blockly.utils.IdGenerator.getNextUniqueId();
this.element_ = element;
// Set class and style
element.className = 'goog-menuitem goog-option ' +
(!this.enabled_ ? 'goog-menuitem-disabled ' : '') +
(this.checked_ ? 'goog-option-selected ' : '') +
(this.rightToLeft_ ? 'goog-menuitem-rtl ' : '');
// goog-menuitem* is deprecated, use blocklyMenuItem*. May 2020.
element.className = 'blocklyMenuItem goog-menuitem ' +
(this.enabled_ ? '' : 'blocklyMenuItemDisabled goog-menuitem-disabled ') +
(this.checked_ ? 'blocklyMenuItemSelected goog-option-selected ' : '') +
(this.rightToLeft_ ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : '');
var content = this.getContentWrapperDom();
var content = document.createElement('div');
content.className = 'blocklyMenuItemContent goog-menuitem-content';
// Add a checkbox for checkable menu items.
if (this.checkable_) {
var checkbox = document.createElement('div');
checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox';
content.appendChild(checkbox);
}
content.appendChild(document.createTextNode(this.content_));
element.appendChild(content);
// Add a checkbox for checkable menu items.
var checkboxDom = this.getCheckboxDom();
if (checkboxDom) {
content.appendChild(checkboxDom);
}
content.appendChild(this.getContentDom());
// Initialize ARIA role and state.
Blockly.utils.aria.setRole(element, this.roleName_ || (this.checkable_ ?
Blockly.utils.aria.Role.MENUITEMCHECKBOX :
Blockly.utils.aria.Role.MENUITEM));
if (this.roleName_) {
Blockly.utils.aria.setRole(element, this.roleName_);
}
Blockly.utils.aria.setState(element, Blockly.utils.aria.State.SELECTED,
(this.checkable_ && this.checked_) || false);
return element;
};
/**
* @return {Element} The HTML element for the checkbox.
* @protected
* Dispose of this menu item.
*/
Blockly.MenuItem.prototype.getCheckboxDom = function() {
if (!this.checkable_) {
return null;
}
var menuItemCheckbox = document.createElement('div');
menuItemCheckbox.className = 'goog-menuitem-checkbox';
return menuItemCheckbox;
Blockly.MenuItem.prototype.dispose = function() {
this.element_ = null;
};
/**
* @return {!Element} The HTML for the content.
* @protected
*/
Blockly.MenuItem.prototype.getContentDom = function() {
var content = this.content_;
if (typeof content === 'string') {
content = document.createTextNode(content);
}
return content;
};
/**
* @return {!Element} The HTML for the content wrapper.
* @protected
*/
Blockly.MenuItem.prototype.getContentWrapperDom = function() {
var contentWrapper = document.createElement('div');
contentWrapper.className = 'goog-menuitem-content';
return contentWrapper;
};
/**
* Sets the content associated with the menu item.
* @param {string} content Text caption to set as the
* menuitem's contents.
* @protected
*/
Blockly.MenuItem.prototype.setContentInternal = function(content) {
this.content_ = content;
};
/**
* Sets the value associated with the menu item.
* @param {*} value Value to be associated with the menu item.
* Gets the menu item's element.
* @return {Element} The DOM element.
* @package
*/
Blockly.MenuItem.prototype.setValue = function(value) {
this.value_ = value;
Blockly.MenuItem.prototype.getElement = function() {
return this.element_;
};
/**
* Gets the unique ID for this menu item.
* @return {string} Unique component ID.
* @package
*/
Blockly.MenuItem.prototype.getId = function() {
return this.element_.id;
};
/**
@@ -152,7 +164,16 @@ Blockly.MenuItem.prototype.getValue = function() {
};
/**
* Set the menu accessibility role.
* Set menu item's rendering direction.
* @param {boolean} rtl True if RTL, false if LTR.
* @package
*/
Blockly.MenuItem.prototype.setRightToLeft = function(rtl) {
this.rightToLeft_ = rtl;
};
/**
* Set the menu item's accessibility role.
* @param {!Blockly.utils.aria.Role} roleName Role name.
* @package
*/
@@ -176,23 +197,7 @@ Blockly.MenuItem.prototype.setCheckable = function(checkable) {
* @package
*/
Blockly.MenuItem.prototype.setChecked = function(checked) {
if (!this.checkable_) {
return;
}
this.checked_ = checked;
var el = this.getElement();
if (el && this.isEnabled()) {
if (checked) {
Blockly.utils.dom.addClass(el, 'goog-option-selected');
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.SELECTED, true);
} else {
Blockly.utils.dom.removeClass(el, 'goog-option-selected');
Blockly.utils.aria.setState(el,
Blockly.utils.aria.State.SELECTED, false);
}
}
};
/**
@@ -205,10 +210,16 @@ Blockly.MenuItem.prototype.setHighlighted = function(highlight) {
var el = this.getElement();
if (el && this.isEnabled()) {
// goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight.
// May 2020.
var name = 'blocklyMenuItemHighlight';
var nameDep = 'goog-menuitem-highlight';
if (highlight) {
Blockly.utils.dom.addClass(el, 'goog-menuitem-highlight');
Blockly.utils.dom.addClass(el, name);
Blockly.utils.dom.addClass(el, nameDep);
} else {
Blockly.utils.dom.removeClass(el, 'goog-menuitem-highlight');
Blockly.utils.dom.removeClass(el, name);
Blockly.utils.dom.removeClass(el, nameDep);
}
}
};
@@ -229,54 +240,26 @@ Blockly.MenuItem.prototype.isEnabled = function() {
*/
Blockly.MenuItem.prototype.setEnabled = function(enabled) {
this.enabled_ = enabled;
var el = this.getElement();
if (el) {
if (!this.enabled_) {
Blockly.utils.dom.addClass(el, 'goog-menuitem-disabled');
} else {
Blockly.utils.dom.removeClass(el, 'goog-menuitem-disabled');
}
}
};
/**
* Handles click events. If the component is enabled, trigger
* the action associated with this menu item.
* @param {Event} _e Mouse event to handle.
* @package
*/
Blockly.MenuItem.prototype.handleClick = function(_e) {
if (this.isEnabled()) {
this.setHighlighted(true);
this.performActionInternal();
}
};
/**
* Performs the appropriate action when the menu item is activated
* by the user.
* @protected
* @package
*/
Blockly.MenuItem.prototype.performActionInternal = function() {
if (this.checkable_) {
this.setChecked(!this.checked_);
}
if (this.actionHandler_) {
this.actionHandler_.call(/** @type {?} */ (this.actionHandlerObj_), this);
Blockly.MenuItem.prototype.performAction = function() {
if (this.isEnabled() && this.actionHandler_) {
this.actionHandler_(this);
}
};
/**
* Set the handler that's triggered when the menu item is activated
* by the user. If `opt_obj` is provided, it will be used as the
* 'this' object in the function when called.
* @param {function(this:T,!Blockly.MenuItem):?} fn The handler.
* @param {T=} opt_obj Used as the 'this' object in f when called.
* @template T
* Set the handler that's called when the menu item is activated by the user.
* `obj` will be used as the 'this' object in the function when called.
* @param {function(!Blockly.MenuItem)} fn The handler.
* @param {!Object} obj Used as the 'this' object in fn when called.
* @package
*/
Blockly.MenuItem.prototype.onAction = function(fn, opt_obj) {
this.actionHandler_ = fn;
this.actionHandlerObj_ = opt_obj;
Blockly.MenuItem.prototype.onAction = function(fn, obj) {
this.actionHandler_ = fn.bind(obj);
};

View File

@@ -91,13 +91,6 @@ Blockly.tree.BaseNode = function(content, config) {
*/
this.expanded_ = false;
/**
* Whether to allow user to collapse this node.
* @type {boolean}
* @protected
*/
this.isUserCollapsible_ = true;
/**
* Nesting depth of this node; cached result of getDepth.
* -1 if value has not been cached.
@@ -174,8 +167,7 @@ Blockly.tree.BaseNode.prototype.initAccessibility = function() {
var ce = this.getChildrenElement();
if (ce) {
Blockly.utils.aria.setRole(ce,
Blockly.utils.aria.Role.GROUP);
Blockly.utils.aria.setRole(ce, Blockly.utils.aria.Role.GROUP);
// In case the children will be created lazily.
if (ce.hasChildNodes()) {
@@ -340,25 +332,24 @@ Blockly.tree.BaseNode.prototype.setDepth_ = function(depth) {
};
/**
* Returns true if the node is a descendant of this node
* @param {Blockly.tree.BaseNode} node The node to check.
* Returns true if the node is a descendant of this node.
* @param {Blockly.Component} node The node to check.
* @return {boolean} True if the node is a descendant of this node, false
* otherwise.
* @protected
*/
Blockly.tree.BaseNode.prototype.contains = function(node) {
var current = node;
while (current) {
if (current == this) {
while (node) {
if (node == this) {
return true;
}
current = current.getParent();
node = node.getParent();
}
return false;
};
/**
* This is re-defined here to indicate to the closure compiler the correct
* This is re-defined here to indicate to the Closure Compiler the correct
* child return type.
* @param {number} index 0-based index.
* @return {Blockly.tree.BaseNode} The child at the given index; null if none.
@@ -617,7 +608,7 @@ Blockly.tree.BaseNode.prototype.getIconDom = function() {
* @protected
*/
Blockly.tree.BaseNode.prototype.getCalculatedIconClass = function() {
throw Error('unimplemented abstract method');
throw Error(Blockly.Component.Error.ABSTRACT_METHOD);
};
/**
@@ -731,16 +722,10 @@ Blockly.tree.BaseNode.prototype.onKeyDown = function(e) {
var handled = true;
switch (e.keyCode) {
case Blockly.utils.KeyCodes.RIGHT:
if (e.altKey) {
break;
}
handled = this.selectChild();
break;
case Blockly.utils.KeyCodes.LEFT:
if (e.altKey) {
break;
}
handled = this.selectParent();
break;
@@ -752,6 +737,12 @@ Blockly.tree.BaseNode.prototype.onKeyDown = function(e) {
handled = this.selectPrevious();
break;
case Blockly.utils.KeyCodes.ENTER:
case Blockly.utils.KeyCodes.SPACE:
this.toggle();
handled = true;
break;
default:
handled = false;
}
@@ -796,7 +787,7 @@ Blockly.tree.BaseNode.prototype.selectPrevious = function() {
* @package
*/
Blockly.tree.BaseNode.prototype.selectParent = function() {
if (this.hasChildren() && this.expanded_ && this.isUserCollapsible_) {
if (this.hasChildren() && this.expanded_) {
this.setExpanded(false);
} else {
var parent = this.getParent();
@@ -847,18 +838,17 @@ Blockly.tree.BaseNode.prototype.getLastShownDescendant = function() {
Blockly.tree.BaseNode.prototype.getNextShownNode = function() {
if (this.hasChildren() && this.expanded_) {
return this.getChildAt(0);
} else {
var parent = this;
var next;
while (parent != this.getTree()) {
next = parent.getNextSibling();
if (next != null) {
return next;
}
parent = parent.getParent();
}
return null;
}
var parent = this;
var next;
while (parent != this.getTree()) {
next = parent.getNextSibling();
if (next != null) {
return next;
}
parent = parent.getParent();
}
return null;
};
/**

View File

@@ -33,20 +33,6 @@ goog.require('Blockly.utils.style');
Blockly.tree.TreeControl = function(toolbox, config) {
this.toolbox_ = toolbox;
/**
* Focus event data.
* @type {?Blockly.EventData}
* @private
*/
this.onFocusWrapper_ = null;
/**
* Blur event data.
* @type {?Blockly.EventData}
* @private
*/
this.onBlurWrapper_ = null;
/**
* Click event data.
* @type {?Blockly.EventData}
@@ -66,7 +52,7 @@ Blockly.tree.TreeControl = function(toolbox, config) {
// The root is open and selected by default.
this.expanded_ = true;
this.selected_ = true;
/**
* Currently selected item.
* @type {Blockly.tree.BaseNode}
@@ -101,41 +87,6 @@ Blockly.tree.TreeControl.prototype.getDepth = function() {
return 0;
};
/**
* Handles focus on the tree.
* @param {!Event} _e The browser event.
* @private
*/
Blockly.tree.TreeControl.prototype.handleFocus_ = function(_e) {
this.focused_ = true;
var el = /** @type {!Element} */ (this.getElement());
Blockly.utils.dom.addClass(el, 'focused');
if (this.selectedItem_) {
this.selectedItem_.select();
}
};
/**
* Handles blur on the tree.
* @param {!Event} _e The browser event.
* @private
*/
Blockly.tree.TreeControl.prototype.handleBlur_ = function(_e) {
this.focused_ = false;
var el = /** @type {!Element} */ (this.getElement());
Blockly.utils.dom.removeClass(el, 'focused');
};
/**
* Get whether this tree has focus or not.
* @return {boolean} True if it has focus.
* @package
*/
Blockly.tree.TreeControl.prototype.hasFocus = function() {
return this.focused_;
};
/** @override */
Blockly.tree.TreeControl.prototype.setExpanded = function(expanded) {
this.expanded_ = expanded;
@@ -278,10 +229,6 @@ Blockly.tree.TreeControl.prototype.attachEvents_ = function() {
var el = this.getElement();
el.tabIndex = 0;
this.onFocusWrapper_ = Blockly.bindEvent_(el,
'focus', this, this.handleFocus_);
this.onBlurWrapper_ = Blockly.bindEvent_(el,
'blur', this, this.handleBlur_);
this.onClickWrapper_ = Blockly.bindEventWithChecks_(el,
'click', this, this.handleMouseEvent_);
this.onKeydownWrapper_ = Blockly.bindEvent_(el,
@@ -293,14 +240,6 @@ Blockly.tree.TreeControl.prototype.attachEvents_ = function() {
* @private
*/
Blockly.tree.TreeControl.prototype.detachEvents_ = function() {
if (this.onFocusWrapper_) {
Blockly.unbindEvent_(this.onFocusWrapper_);
this.onFocusWrapper_ = null;
}
if (this.onBlurWrapper_) {
Blockly.unbindEvent_(this.onBlurWrapper_);
this.onBlurWrapper_ = null;
}
if (this.onClickWrapper_) {
Blockly.unbindEvent_(this.onClickWrapper_);
this.onClickWrapper_ = null;
@@ -318,12 +257,8 @@ Blockly.tree.TreeControl.prototype.detachEvents_ = function() {
*/
Blockly.tree.TreeControl.prototype.handleMouseEvent_ = function(e) {
var node = this.getNodeFromEvent_(e);
if (node) {
switch (e.type) {
case 'click':
node.onClick_(e);
break;
}
if (node && e.type == 'click') {
node.onClick_(e);
}
};
@@ -334,10 +269,8 @@ Blockly.tree.TreeControl.prototype.handleMouseEvent_ = function(e) {
* @private
*/
Blockly.tree.TreeControl.prototype.handleKeyEvent_ = function(e) {
var handled = false;
// Handle navigation keystrokes.
handled = (this.selectedItem_ && this.selectedItem_.onKeyDown(e)) || handled;
var handled = !!(this.selectedItem_ && this.selectedItem_.onKeyDown(e));
if (handled) {
Blockly.utils.style.scrollIntoContainerView(
@@ -360,7 +293,7 @@ Blockly.tree.TreeControl.prototype.getNodeFromEvent_ = function(e) {
// find the right node
var node = null;
var target = e.target;
while (target != null) {
while (target) {
var id = target.id;
node = Blockly.tree.BaseNode.allNodes[id];
if (node) {

View File

@@ -93,7 +93,7 @@ Blockly.tree.TreeNode.prototype.getCalculatedIconClass = function() {
*/
Blockly.tree.TreeNode.prototype.onClick_ = function(_e) {
// Expand icon.
if (this.hasChildren() && this.isUserCollapsible_) {
if (this.hasChildren()) {
this.toggle();
this.select();
} else if (this.isSelected()) {

View File

@@ -36,11 +36,11 @@ goog.require('Blockly.Xml');
Blockly.ContextMenu.currentBlock = null;
/**
* Opaque data that can be passed to unbindEvent_.
* @type {Array.<!Array>}
* Menu object.
* @type {Blockly.Menu}
* @private
*/
Blockly.ContextMenu.eventWrapper_ = null;
Blockly.ContextMenu.menu_ = null;
/**
* Construct the menu based on the list of options and show the menu.
@@ -49,17 +49,18 @@ Blockly.ContextMenu.eventWrapper_ = null;
* @param {boolean} rtl True if RTL, false if LTR.
*/
Blockly.ContextMenu.show = function(e, options, rtl) {
Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, null);
Blockly.WidgetDiv.show(Blockly.ContextMenu, rtl, Blockly.ContextMenu.dispose);
if (!options.length) {
Blockly.ContextMenu.hide();
return;
}
var menu = Blockly.ContextMenu.populate_(options, rtl);
Blockly.ContextMenu.menu_ = menu;
Blockly.ContextMenu.position_(menu, e, rtl);
// 1ms delay is required for focusing on context menus because some other
// mouse event is still waiting in the queue and clears focus.
setTimeout(function() {menu.getElement().focus();}, 1);
setTimeout(function() {menu.focus();}, 1);
Blockly.ContextMenu.currentBlock = null; // May be set by Blockly.Block.
};
@@ -77,14 +78,15 @@ Blockly.ContextMenu.populate_ = function(options, rtl) {
callback: Blockly.MakeItSo}
*/
var menu = new Blockly.Menu();
menu.setRightToLeft(rtl);
menu.setRole(Blockly.utils.aria.Role.MENU);
for (var i = 0, option; (option = options[i]); i++) {
var menuItem = new Blockly.MenuItem(option.text);
menuItem.setRightToLeft(rtl);
menu.addChild(menuItem, true);
menuItem.setRole(Blockly.utils.aria.Role.MENUITEM);
menu.addChild(menuItem);
menuItem.setEnabled(option.enabled);
if (option.enabled) {
var actionHandler = function() {
var actionHandler = function(_menuItem) {
var option = this;
Blockly.ContextMenu.hide();
option.callback();
@@ -126,7 +128,7 @@ Blockly.ContextMenu.position_ = function(menu, e, rtl) {
// Calling menuDom.focus() has to wait until after the menu has been placed
// correctly. Otherwise it will cause a page scroll to get the misplaced menu
// in view. See issue #1329.
menu.getElement().focus();
menu.focus();
};
/**
@@ -153,9 +155,15 @@ Blockly.ContextMenu.createWidget_ = function(menu) {
Blockly.ContextMenu.hide = function() {
Blockly.WidgetDiv.hideIfOwner(Blockly.ContextMenu);
Blockly.ContextMenu.currentBlock = null;
if (Blockly.ContextMenu.eventWrapper_) {
Blockly.unbindEvent_(Blockly.ContextMenu.eventWrapper_);
Blockly.ContextMenu.eventWrapper_ = null;
};
/**
* Dispose of the menu.
*/
Blockly.ContextMenu.dispose = function() {
if (Blockly.ContextMenu.menu_) {
Blockly.ContextMenu.menu_.dispose();
Blockly.ContextMenu.menu_ = null;
}
};

View File

@@ -74,13 +74,13 @@ Blockly.Css.inject = function(hasCss, pathToMedia) {
/**
* Set the cursor to be displayed when over something draggable.
* See See https://github.com/google/blockly/issues/981 for context.
* See https://github.com/google/blockly/issues/981 for context.
* @param {*} _cursor Enum.
* @deprecated April 2017.
*/
Blockly.Css.setCursor = function(_cursor) {
console.warn('Deprecated call to Blockly.Css.setCursor. ' +
'See https://github.com/google/blockly/issues/981 for context');
'See issue #981 for context');
};
/**
@@ -150,8 +150,7 @@ Blockly.Css.CONTENT = [
'box-shadow: 4px 4px 20px 1px rgba(0,0,0,.15);',
'color: #000;',
'display: none;',
'font-family: sans-serif;',
'font-size: 9pt;',
'font: 9pt sans-serif;',
'opacity: .9;',
'padding: 2px;',
'position: absolute;',
@@ -169,11 +168,11 @@ Blockly.Css.CONTENT = [
'background-color: #fff;',
'border-radius: 2px;',
'padding: 4px;',
'box-shadow: 0px 0px 3px 1px rgba(0,0,0,.3);',
'box-shadow: 0 0 3px 1px rgba(0,0,0,.3);',
'}',
'.blocklyDropDownDiv.focused {',
'box-shadow: 0px 0px 6px 1px rgba(0,0,0,.3);',
'.blocklyDropDownDiv.blocklyFocused {',
'box-shadow: 0 0 6px 1px rgba(0,0,0,.3);',
'}',
'.blocklyDropDownContent {',
@@ -309,7 +308,7 @@ Blockly.Css.CONTENT = [
'.blocklyInsertionMarker>.blocklyPathLight,',
'.blocklyInsertionMarker>.blocklyPathDark {',
'fill-opacity: .2;',
'stroke: none',
'stroke: none;',
'}',
'.blocklyMultilineText {',
@@ -333,7 +332,8 @@ Blockly.Css.CONTENT = [
Don't allow users to select text. It gets annoying when trying to
drag a block and selected text moves instead.
*/
'.blocklySvg text, .blocklyBlockDragSurface text {',
'.blocklySvg text,',
'.blocklyBlockDragSurface text {',
'user-select: none;',
'-ms-user-select: none;',
'-webkit-user-select: none;',
@@ -416,7 +416,8 @@ Blockly.Css.CONTENT = [
'z-index: 30;',
'}',
'.blocklyScrollbarHorizontal, .blocklyScrollbarVertical {',
'.blocklyScrollbarHorizontal,',
'.blocklyScrollbarVertical {',
'position: absolute;',
'outline: none;',
'}',
@@ -449,217 +450,10 @@ Blockly.Css.CONTENT = [
'background: #faa;',
'}',
'.blocklyContextMenu {',
'border-radius: 4px;',
'max-height: 100%;',
'}',
'.blocklyDropdownMenu {',
'border-radius: 2px;',
'padding: 0 !important;',
'}',
'.blocklyWidgetDiv .blocklyDropdownMenu .goog-menuitem,',
'.blocklyDropDownDiv .blocklyDropdownMenu .goog-menuitem {',
/* 28px on the left for icon or checkbox. */
'padding-left: 28px;',
'}',
/* BiDi override for the resting state. */
/* #noflip */
'.blocklyWidgetDiv .blocklyDropdownMenu .goog-menuitem.goog-menuitem-rtl,',
'.blocklyDropDownDiv .blocklyDropdownMenu .goog-menuitem.goog-menuitem-rtl {',
/* Flip left/right padding for BiDi. */
'padding-left: 5px;',
'padding-right: 28px;',
'}',
'.blocklyVerticalMarker {',
'stroke-width: 3px;',
'fill: rgba(255,255,255,.5);',
'pointer-events: none',
'}',
'.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,',
'.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon,',
'.blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox,',
'.blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {',
'background: url(<<<PATH>>>/sprites.png) no-repeat -48px -16px;',
'}',
/* Copied from: goog/css/menu.css */
/*
* Copyright 2009 The Closure Library Authors. All Rights Reserved.
*
* Use of this source code is governed by the Apache License, Version 2.0.
* See the COPYING file for details.
*/
/**
* Standard styling for menus created by goog.ui.MenuRenderer.
*
* @author attila@google.com (Attila Bodis)
*/
'.blocklyWidgetDiv .goog-menu {',
'background: #fff;',
'border-color: transparent;',
'border-style: solid;',
'border-width: 1px;',
'cursor: default;',
'font: normal 13px Arial, sans-serif;',
'margin: 0;',
'outline: none;',
'padding: 4px 0;',
'position: absolute;',
'overflow-y: auto;',
'overflow-x: hidden;',
'max-height: 100%;',
'z-index: 20000;', /* Arbitrary, but some apps depend on it... */
'box-shadow: 0px 0px 3px 1px rgba(0,0,0,.3);',
'}',
'.blocklyWidgetDiv .goog-menu.focused {',
'box-shadow: 0px 0px 6px 1px rgba(0,0,0,.3);',
'}',
'.blocklyDropDownDiv .goog-menu {',
'cursor: default;',
'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;',
'outline: none;',
'z-index: 20000;', /* Arbitrary, but some apps depend on it... */
'}',
/* Copied from: goog/css/menuitem.css */
/*
* Copyright 2009 The Closure Library Authors. All Rights Reserved.
*
* Use of this source code is governed by the Apache License, Version 2.0.
* See the COPYING file for details.
*/
/**
* Standard styling for menus created by goog.ui.MenuItemRenderer.
*
* @author attila@google.com (Attila Bodis)
*/
/**
* State: resting.
*
* NOTE(mleibman,chrishenry):
* The RTL support in Closure is provided via two mechanisms -- "rtl" CSS
* classes and BiDi flipping done by the CSS compiler. Closure supports RTL
* with or without the use of the CSS compiler. In order for them not to
* conflict with each other, the "rtl" CSS classes need to have the #noflip
* annotation. The non-rtl counterparts should ideally have them as well,
* but, since .goog-menuitem existed without .goog-menuitem-rtl for so long
* before being added, there is a risk of people having templates where they
* are not rendering the .goog-menuitem-rtl class when in RTL and instead
* rely solely on the BiDi flipping by the CSS compiler. That's why we're
* not adding the #noflip to .goog-menuitem.
*/
'.blocklyWidgetDiv .goog-menuitem,',
'.blocklyDropDownDiv .goog-menuitem {',
'color: #000;',
'font: normal 13px Arial, sans-serif;',
'list-style: none;',
'margin: 0;',
/* 7em on the right for shortcut. */
'min-width: 7em;',
'border: none;',
'padding: 6px 15px;',
'white-space: nowrap;',
'cursor: pointer;',
'}',
/* If a menu doesn't have checkable items or items with icons,
* remove padding.
*/
'.blocklyWidgetDiv .goog-menu-nocheckbox .goog-menuitem,',
'.blocklyWidgetDiv .goog-menu-noicon .goog-menuitem,',
'.blocklyDropDownDiv .goog-menu-nocheckbox .goog-menuitem,',
'.blocklyDropDownDiv .goog-menu-noicon .goog-menuitem {',
'padding-left: 12px;',
'}',
'.blocklyWidgetDiv .goog-menuitem-content,',
'.blocklyDropDownDiv .goog-menuitem-content {',
'font-family: Arial, sans-serif;',
'font-size: 13px;',
'}',
'.blocklyWidgetDiv .goog-menuitem-content {',
'color: #000;',
'}',
'.blocklyDropDownDiv .goog-menuitem-content {',
'color: #000;',
'}',
/* State: disabled. */
'.blocklyWidgetDiv .goog-menuitem-disabled,',
'.blocklyDropDownDiv .goog-menuitem-disabled {',
'cursor: inherit;',
'}',
'.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-content,',
'.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-content {',
'color: #ccc !important;',
'}',
'.blocklyWidgetDiv .goog-menuitem-disabled .goog-menuitem-icon,',
'.blocklyDropDownDiv .goog-menuitem-disabled .goog-menuitem-icon {',
'opacity: .3;',
'filter: alpha(opacity=30);',
'}',
/* State: hover. */
'.blocklyWidgetDiv .goog-menuitem-highlight ,',
'.blocklyDropDownDiv .goog-menuitem-highlight {',
'background-color: rgba(0,0,0,.1);',
'}',
/* State: selected/checked. */
'.blocklyWidgetDiv .goog-menuitem-checkbox,',
'.blocklyWidgetDiv .goog-menuitem-icon,',
'.blocklyDropDownDiv .goog-menuitem-checkbox,',
'.blocklyDropDownDiv .goog-menuitem-icon {',
'background-repeat: no-repeat;',
'height: 16px;',
'left: 6px;',
'position: absolute;',
'right: auto;',
'vertical-align: middle;',
'width: 16px;',
'}',
/* BiDi override for the selected/checked state. */
/* #noflip */
'.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,',
'.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon,',
'.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox,',
'.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {',
/* Flip left/right positioning. */
'left: auto;',
'right: 6px;',
'}',
'.blocklyWidgetDiv .goog-option-selected .goog-menuitem-checkbox,',
'.blocklyWidgetDiv .goog-option-selected .goog-menuitem-icon,',
'.blocklyDropDownDiv .goog-option-selected .goog-menuitem-checkbox,',
'.blocklyDropDownDiv .goog-option-selected .goog-menuitem-icon {',
'position: static;', /* Scroll with the menu. */
'float: left;',
'margin-left: -24px;',
'}',
'.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-checkbox,',
'.blocklyWidgetDiv .goog-menuitem-rtl .goog-menuitem-icon,',
'.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-checkbox,',
'.blocklyDropDownDiv .goog-menuitem-rtl .goog-menuitem-icon {',
'float: right;',
'margin-right: -24px;',
'pointer-events: none;',
'}',
'.blocklyComputeCanvas {',
@@ -671,5 +465,95 @@ Blockly.Css.CONTENT = [
'.blocklyNoPointerEvents {',
'pointer-events: none;',
'}',
'.blocklyContextMenu {',
'border-radius: 4px;',
'max-height: 100%;',
'}',
'.blocklyDropdownMenu {',
'border-radius: 2px;',
'padding: 0 !important;',
'}',
'.blocklyDropdownMenu .blocklyMenuItem {',
/* 28px on the left for icon or checkbox. */
'padding-left: 28px;',
'}',
/* BiDi override for the resting state. */
'.blocklyDropdownMenu .blocklyMenuItemRtl {',
/* Flip left/right padding for BiDi. */
'padding-left: 5px;',
'padding-right: 28px;',
'}',
'.blocklyWidgetDiv .blocklyMenu {',
'background: #fff;',
'border: 1px solid transparent;',
'box-shadow: 0 0 3px 1px rgba(0,0,0,.3);',
'font: normal 13px Arial, sans-serif;',
'margin: 0;',
'outline: none;',
'padding: 4px 0;',
'position: absolute;',
'overflow-y: auto;',
'overflow-x: hidden;',
'max-height: 100%;',
'z-index: 20000;', /* Arbitrary, but some apps depend on it... */
'}',
'.blocklyWidgetDiv .blocklyMenu.blocklyFocused {',
'box-shadow: 0 0 6px 1px rgba(0,0,0,.3);',
'}',
'.blocklyDropDownDiv .blocklyMenu {',
'font: normal 13px "Helvetica Neue", Helvetica, sans-serif;',
'outline: none;',
'z-index: 20000;', /* Arbitrary, but some apps depend on it... */
'}',
/* State: resting. */
'.blocklyMenuItem {',
'border: none;',
'color: #000;',
'cursor: pointer;',
'list-style: none;',
'margin: 0;',
/* 7em on the right for shortcut. */
'min-width: 7em;',
'padding: 6px 15px;',
'white-space: nowrap;',
'}',
/* State: disabled. */
'.blocklyMenuItemDisabled {',
'color: #ccc;',
'cursor: inherit;',
'}',
/* State: hover. */
'.blocklyMenuItemHighlight {',
'background-color: rgba(0,0,0,.1);',
'}',
/* State: selected/checked. */
'.blocklyMenuItemCheckbox {',
'height: 16px;',
'position: absolute;',
'width: 16px;',
'}',
'.blocklyMenuItemSelected .blocklyMenuItemCheckbox {',
'background: url(<<<PATH>>>/sprites.png) no-repeat -48px -16px;',
'float: left;',
'margin-left: -24px;',
'position: static;', /* Scroll with the menu. */
'}',
'.blocklyMenuItemRtl .blocklyMenuItemCheckbox {',
'float: right;',
'margin-right: -24px;',
'}',
/* eslint-enable indent */
];

View File

@@ -166,10 +166,10 @@ Blockly.DropDownDiv.createDom = function() {
// Handle focusin/out events to add a visual indicator when
// a child is focused or blurred.
div.addEventListener('focusin', function() {
Blockly.utils.dom.addClass(div, 'focused');
Blockly.utils.dom.addClass(div, 'blocklyFocused');
});
div.addEventListener('focusout', function() {
Blockly.utils.dom.removeClass(div, 'focused');
Blockly.utils.dom.removeClass(div, 'blocklyFocused');
});
};
@@ -184,7 +184,7 @@ Blockly.DropDownDiv.setBoundsElement = function(boundsElement) {
/**
* Provide the div for inserting content into the drop-down.
* @return {Element} Div to populate with content
* @return {!Element} Div to populate with content.
*/
Blockly.DropDownDiv.getContentDiv = function() {
return Blockly.DropDownDiv.content_;

View File

@@ -270,8 +270,12 @@ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) {
}
// Element gets created in render.
this.menu_.render(Blockly.DropDownDiv.getContentDiv());
Blockly.utils.dom.addClass(
/** @type {!Element} */ (this.menu_.getElement()), 'blocklyDropdownMenu');
var menuElement = /** @type {!Element} */ (this.menu_.getElement());
Blockly.utils.dom.addClass(menuElement, 'blocklyDropdownMenu');
Blockly.utils.aria.setState(menuElement,
Blockly.utils.aria.State.ACTIVEDESCENDANT,
this.selectedMenuItem_ ? this.selectedMenuItem_.getId() : '');
if (this.getConstants().FIELD_DROPDOWN_COLOURED_DIV) {
var primaryColour = (this.sourceBlock_.isShadow()) ?
@@ -295,7 +299,7 @@ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) {
if (this.selectedMenuItem_) {
Blockly.utils.style.scrollIntoContainerView(
/** @type {!Element} */ (this.selectedMenuItem_.getElement()),
/** @type {!Element} */ (this.menu_.getElement()));
menuElement);
}
this.applyColour();
@@ -308,7 +312,6 @@ Blockly.FieldDropdown.prototype.showEditor_ = function(opt_e) {
*/
Blockly.FieldDropdown.prototype.dropdownCreate_ = function() {
var menu = new Blockly.Menu();
menu.setRightToLeft(this.sourceBlock_.RTL);
menu.setRole(Blockly.utils.aria.Role.LISTBOX);
var options = this.getOptions(false);
@@ -323,12 +326,11 @@ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() {
image.alt = content['alt'] || '';
content = image;
}
var menuItem = new Blockly.MenuItem(content);
var menuItem = new Blockly.MenuItem(content, value);
menuItem.setRole(Blockly.utils.aria.Role.OPTION);
menuItem.setRightToLeft(this.sourceBlock_.RTL);
menuItem.setValue(value);
menuItem.setCheckable(true);
menu.addChild(menuItem, true);
menu.addChild(menuItem);
menuItem.setChecked(value == this.value_);
if (value == this.value_) {
this.selectedMenuItem_ = menuItem;
@@ -336,10 +338,6 @@ Blockly.FieldDropdown.prototype.dropdownCreate_ = function() {
menuItem.onAction(this.handleMenuActionEvent_, this);
}
Blockly.utils.aria.setState(/** @type {!Element} */ (menu.getElement()),
Blockly.utils.aria.State.ACTIVEDESCENDANT,
this.selectedMenuItem_ ? this.selectedMenuItem_.getId() : '');
return menu;
};

View File

@@ -166,7 +166,7 @@ Blockly.FlyoutButton.prototype.createDom = function() {
var fontMetrics = Blockly.utils.dom.measureFontMetrics(text, fontSize,
fontWeight, fontFamily);
this.height = fontMetrics.height;
if (!this.isLabel_) {
this.width += 2 * Blockly.FlyoutButton.MARGIN_X;
this.height += 2 * Blockly.FlyoutButton.MARGIN_Y;

View File

@@ -71,7 +71,7 @@ Blockly.BasicCursor.prototype.prev = function() {
return null;
}
var newNode = this.getPreviousNode_(curNode, this.validNode_);
if (newNode) {
this.setCurNode(newNode);
}

View File

@@ -23,7 +23,6 @@ goog.require('Blockly.navigation');
* @constructor
*/
Blockly.Marker = function() {
/**
* The colour of the marker.
* @type {?string}
@@ -119,4 +118,3 @@ Blockly.Marker.prototype.dispose = function() {
this.getDrawer().dispose();
}
};

View File

@@ -256,7 +256,7 @@ Blockly.blockRendering.ConstantProvider = function() {
* @type {number}
*/
this.FIELD_TEXT_HEIGHT = -1; // Dynamically set
/**
* Text baseline. This constant is dynamically set in ``setFontConstants_``
* to be the baseline of the text based on the font used.
@@ -1168,9 +1168,8 @@ Blockly.blockRendering.ConstantProvider.prototype.getCSS_ = function(selector) {
// Text.
selector + ' .blocklyText, ',
selector + ' .blocklyFlyoutLabelText {',
'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';',
'font-size: ' + this.FIELD_TEXT_FONTSIZE + 'pt;',
'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';',
'font: ' + this.FIELD_TEXT_FONTWEIGHT + ' ' +
this.FIELD_TEXT_FONTSIZE + 'pt ' + this.FIELD_TEXT_FONTFAMILY + ';',
'}',
// Fields.
@@ -1233,7 +1232,7 @@ Blockly.blockRendering.ConstantProvider.prototype.getCSS_ = function(selector) {
// Insertion marker.
selector + ' .blocklyInsertionMarker>.blocklyPath {',
'fill-opacity: ' + this.INSERTION_MARKER_OPACITY + ';',
'stroke: none',
'stroke: none;',
'}',
/* eslint-enable indent */
];

View File

@@ -581,7 +581,7 @@ Blockly.blockRendering.RenderInfo.prototype.addAlignmentPadding_ = function(row,
if (row.hasExternalInput || row.hasStatement) {
row.widthWithConnectedBlocks += missingSpace;
}
// Decide where the extra padding goes.
if (row.align == Blockly.ALIGN_LEFT) {
// Add padding to the end of the row.

View File

@@ -57,7 +57,7 @@ Blockly.geras.ConstantProvider.prototype.getCSS_ = function(selector) {
selector + ' .blocklyInsertionMarker>.blocklyPathLight,',
selector + ' .blocklyInsertionMarker>.blocklyPathDark {',
'fill-opacity: ' + this.INSERTION_MARKER_OPACITY + ';',
'stroke: none',
'stroke: none;',
'}',
/* eslint-enable indent */
]);

View File

@@ -122,7 +122,7 @@ Blockly.geras.PathObject.prototype.applyColour = function(block) {
this.svgPathDark.setAttribute('fill', this.colourDark);
Blockly.geras.PathObject.superClass_.applyColour.call(this, block);
this.svgPath.setAttribute('stroke', 'none');
};

View File

@@ -69,7 +69,7 @@ Blockly.zelos.ConstantProvider = function() {
* @override
*/
this.NOTCH_OFFSET_LEFT = 3 * this.GRID_UNIT;
/**
* @override
*/
@@ -259,7 +259,7 @@ Blockly.zelos.ConstantProvider = function() {
* @override
*/
this.FIELD_BORDER_RECT_X_PADDING = 2 * this.GRID_UNIT;
/**
* @override
*/
@@ -905,13 +905,12 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) {
return [
/* eslint-disable indent */
// Text.
selector + ' .blocklyText, ',
selector + ' .blocklyText,',
selector + ' .blocklyFlyoutLabelText {',
'font-family: ' + this.FIELD_TEXT_FONTFAMILY + ';',
'font-size: ' + this.FIELD_TEXT_FONTSIZE + 'pt;',
'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';',
'font: ' + this.FIELD_TEXT_FONTWEIGHT + ' ' +
this.FIELD_TEXT_FONTSIZE + 'pt ' + this.FIELD_TEXT_FONTFAMILY + ';',
'}',
// Fields.
selector + ' .blocklyText {',
'fill: #fff;',
@@ -926,7 +925,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) {
selector + ' .blocklyEditableText>g>text {',
'fill: #575E75;',
'}',
// Flyout labels.
selector + ' .blocklyFlyoutLabelText {',
'fill: #575E75;',
@@ -939,7 +938,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) {
// Editable field hover.
selector + ' .blocklyDraggable:not(.blocklyDisabled)',
' .blocklyEditableText:not(.editing):hover>rect ,',
' .blocklyEditableText:not(.editing):hover>rect,',
selector + ' .blocklyDraggable:not(.blocklyDisabled)',
' .blocklyEditableText:not(.editing):hover>.blocklyPath {',
'stroke: #fff;',
@@ -952,7 +951,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) {
'font-weight: ' + this.FIELD_TEXT_FONTWEIGHT + ';',
'color: #575E75;',
'}',
// Dropdown field.
selector + ' .blocklyDropdownText {',
'fill: #fff !important;',
@@ -979,7 +978,7 @@ Blockly.zelos.ConstantProvider.prototype.getCSS_ = function(selector) {
// Insertion marker.
selector + ' .blocklyInsertionMarker>.blocklyPath {',
'fill-opacity: ' + this.INSERTION_MARKER_OPACITY + ';',
'stroke: none',
'stroke: none;',
'}',
/* eslint-enable indent */
];

View File

@@ -210,7 +210,7 @@ Blockly.Theme.defineTheme = function(name, themeObj) {
Blockly.utils.object.deepMerge(theme, base);
theme.name = name;
}
Blockly.utils.object.deepMerge(theme.blockStyles,
themeObj['blockStyles']);
Blockly.utils.object.deepMerge(theme.categoryStyles,

View File

@@ -180,7 +180,7 @@ Blockly.Toolbox.prototype.init = function() {
'rendererOverrides': workspace.options.rendererOverrides
}));
workspaceOptions.toolboxPosition = workspace.options.toolboxPosition;
if (workspace.horizontalLayout) {
if (!Blockly.HorizontalFlyout) {
throw Error('Missing require for Blockly.HorizontalFlyout');
@@ -196,7 +196,7 @@ Blockly.Toolbox.prototype.init = function() {
throw Error('One of Blockly.VerticalFlyout or Blockly.Horizontal must be' +
'required.');
}
// Insert the flyout after the workspace.
Blockly.utils.dom.insertAfter(this.flyout_.createDom('svg'), svg);
this.flyout_.init(workspace);
@@ -830,8 +830,7 @@ Blockly.Css.register([
'.blocklyTreeLabel {',
'cursor: default;',
'font-family: sans-serif;',
'font-size: 16px;',
'font: 16px sans-serif;',
'padding: 0 3px;',
'vertical-align: middle;',
'}',

View File

@@ -110,7 +110,7 @@ Blockly.WorkspaceAudio.prototype.preload = function() {
} else {
sound.pause();
}
// iOS can only process one sound at a time. Trying to load more than one
// corrupts the earlier ones. Just load one and leave the others uncached.
if (Blockly.utils.userAgent.IPAD || Blockly.utils.userAgent.IPHONE) {

View File

@@ -640,7 +640,7 @@ Blockly.Css.register([
'.blocklyCommentRect {',
'fill: #E7DE8E;',
'stroke: #bcA903;',
'stroke-width: 1px',
'stroke-width: 1px;',
'}',
'.blocklyCommentTarget {',
@@ -673,11 +673,11 @@ Blockly.Css.register([
'.blocklyCommentDeleteIcon {',
'cursor: pointer;',
'fill: #000;',
'display: none',
'display: none;',
'}',
'.blocklySelected > .blocklyCommentDeleteIcon {',
'display: block',
'display: block;',
'}',
'.blocklyDeleteIconShape {',