mirror of
https://github.com/google/blockly.git
synced 2026-01-09 10:00:09 +01:00
refactor: convert some files to es6 classes (#5863)
* refactor: convert menu.js to ES6 class * refactor: convert menuitem.js to ES6 class * refactor: convert scrollbar_pair.js to ES6 class * chore: define properties in the constructor in names.js * refactor: convert names.js to ES6 class * chore: run clang-format
This commit is contained in:
800
core/menu.js
800
core/menu.js
@@ -29,449 +29,453 @@ const {Size} = goog.requireType('Blockly.utils.Size');
|
||||
|
||||
/**
|
||||
* A basic menu class.
|
||||
* @constructor
|
||||
* @alias Blockly.Menu
|
||||
*/
|
||||
const Menu = function() {
|
||||
const Menu = class {
|
||||
/**
|
||||
* 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<MenuItem>}
|
||||
* @private
|
||||
* @alias Blockly.Menu
|
||||
*/
|
||||
this.menuItems_ = [];
|
||||
constructor() {
|
||||
/**
|
||||
* 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<MenuItem>}
|
||||
* @private
|
||||
*/
|
||||
this.menuItems_ = [];
|
||||
|
||||
/**
|
||||
* Coordinates of the mousedown event that caused this menu to open. Used to
|
||||
* prevent the consequent mouseup event due to a simple click from
|
||||
* activating a menu item immediately.
|
||||
* @type {?Coordinate}
|
||||
* @package
|
||||
*/
|
||||
this.openingCoords = null;
|
||||
|
||||
/**
|
||||
* This is the element that we will listen to the real focus events on.
|
||||
* A value of null means no menu item is highlighted.
|
||||
* @type {?MenuItem}
|
||||
* @private
|
||||
*/
|
||||
this.highlightedItem_ = null;
|
||||
|
||||
/**
|
||||
* Mouse over event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.mouseOverHandler_ = null;
|
||||
|
||||
/**
|
||||
* Click event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.clickHandler_ = null;
|
||||
|
||||
/**
|
||||
* Mouse enter event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.mouseEnterHandler_ = null;
|
||||
|
||||
/**
|
||||
* Mouse leave event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.mouseLeaveHandler_ = null;
|
||||
|
||||
/**
|
||||
* Key down event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.onKeyDownHandler_ = null;
|
||||
|
||||
/**
|
||||
* The menu's root DOM element.
|
||||
* @type {?Element}
|
||||
* @private
|
||||
*/
|
||||
this.element_ = null;
|
||||
|
||||
/**
|
||||
* ARIA name for this menu.
|
||||
* @type {?aria.Role}
|
||||
* @private
|
||||
*/
|
||||
this.roleName_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinates of the mousedown event that caused this menu to open. Used to
|
||||
* prevent the consequent mouseup event due to a simple click from activating
|
||||
* a menu item immediately.
|
||||
* @type {?Coordinate}
|
||||
* Add a new menu item to the bottom of this menu.
|
||||
* @param {!MenuItem} menuItem Menu item to append.
|
||||
*/
|
||||
addChild(menuItem) {
|
||||
this.menuItems_.push(menuItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the menu DOM.
|
||||
* @param {!Element} container Element upon which to append this menu.
|
||||
*/
|
||||
render(container) {
|
||||
const element =
|
||||
/** @type {!HTMLDivElement} */ (document.createElement('div'));
|
||||
// goog-menu is deprecated, use blocklyMenu. May 2020.
|
||||
element.className = 'blocklyMenu goog-menu blocklyNonSelectable';
|
||||
element.tabIndex = 0;
|
||||
if (this.roleName_) {
|
||||
aria.setRole(element, this.roleName_);
|
||||
}
|
||||
this.element_ = element;
|
||||
|
||||
// Add menu items.
|
||||
for (let i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
|
||||
element.appendChild(menuItem.createDom());
|
||||
}
|
||||
|
||||
// Add event handlers.
|
||||
this.mouseOverHandler_ = browserEvents.conditionalBind(
|
||||
element, 'mouseover', this, this.handleMouseOver_, true);
|
||||
this.clickHandler_ = browserEvents.conditionalBind(
|
||||
element, 'click', this, this.handleClick_, true);
|
||||
this.mouseEnterHandler_ = browserEvents.conditionalBind(
|
||||
element, 'mouseenter', this, this.handleMouseEnter_, true);
|
||||
this.mouseLeaveHandler_ = browserEvents.conditionalBind(
|
||||
element, 'mouseleave', this, this.handleMouseLeave_, true);
|
||||
this.onKeyDownHandler_ = browserEvents.conditionalBind(
|
||||
element, 'keydown', this, this.handleKeyEvent_);
|
||||
|
||||
container.appendChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the menu's element.
|
||||
* @return {?Element} The DOM element.
|
||||
* @package
|
||||
*/
|
||||
this.openingCoords = null;
|
||||
getElement() {
|
||||
return this.element_;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the element that we will listen to the real focus events on.
|
||||
* A value of null means no menu item is highlighted.
|
||||
* @type {?MenuItem}
|
||||
* @private
|
||||
* Focus the menu element.
|
||||
* @package
|
||||
*/
|
||||
this.highlightedItem_ = null;
|
||||
focus() {
|
||||
const el = this.getElement();
|
||||
if (el) {
|
||||
el.focus({preventScroll: true});
|
||||
dom.addClass(el, 'blocklyFocused');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse over event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* Blur the menu element.
|
||||
* @private
|
||||
*/
|
||||
this.mouseOverHandler_ = null;
|
||||
blur_() {
|
||||
const el = this.getElement();
|
||||
if (el) {
|
||||
el.blur();
|
||||
dom.removeClass(el, 'blocklyFocused');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
* Set the menu accessibility role.
|
||||
* @param {!aria.Role} roleName role name.
|
||||
* @package
|
||||
*/
|
||||
this.clickHandler_ = null;
|
||||
setRole(roleName) {
|
||||
this.roleName_ = roleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse enter event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
* Dispose of this menu.
|
||||
*/
|
||||
this.mouseEnterHandler_ = null;
|
||||
dispose() {
|
||||
// Remove event handlers.
|
||||
if (this.mouseOverHandler_) {
|
||||
browserEvents.unbind(this.mouseOverHandler_);
|
||||
this.mouseOverHandler_ = null;
|
||||
}
|
||||
if (this.clickHandler_) {
|
||||
browserEvents.unbind(this.clickHandler_);
|
||||
this.clickHandler_ = null;
|
||||
}
|
||||
if (this.mouseEnterHandler_) {
|
||||
browserEvents.unbind(this.mouseEnterHandler_);
|
||||
this.mouseEnterHandler_ = null;
|
||||
}
|
||||
if (this.mouseLeaveHandler_) {
|
||||
browserEvents.unbind(this.mouseLeaveHandler_);
|
||||
this.mouseLeaveHandler_ = null;
|
||||
}
|
||||
if (this.onKeyDownHandler_) {
|
||||
browserEvents.unbind(this.onKeyDownHandler_);
|
||||
this.onKeyDownHandler_ = null;
|
||||
}
|
||||
|
||||
// Remove menu items.
|
||||
for (let i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
|
||||
menuItem.dispose();
|
||||
}
|
||||
this.element_ = null;
|
||||
}
|
||||
|
||||
// Child component management.
|
||||
|
||||
/**
|
||||
* Mouse leave event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* 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 {?MenuItem} Menu item for which the DOM element belongs to.
|
||||
* @private
|
||||
*/
|
||||
this.mouseLeaveHandler_ = null;
|
||||
|
||||
/**
|
||||
* Key down event data.
|
||||
* @type {?browserEvents.Data}
|
||||
* @private
|
||||
*/
|
||||
this.onKeyDownHandler_ = null;
|
||||
|
||||
/**
|
||||
* The menu's root DOM element.
|
||||
* @type {?Element}
|
||||
* @private
|
||||
*/
|
||||
this.element_ = null;
|
||||
|
||||
/**
|
||||
* ARIA name for this menu.
|
||||
* @type {?aria.Role}
|
||||
* @private
|
||||
*/
|
||||
this.roleName_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add a new menu item to the bottom of this menu.
|
||||
* @param {!MenuItem} menuItem Menu item to append.
|
||||
*/
|
||||
Menu.prototype.addChild = function(menuItem) {
|
||||
this.menuItems_.push(menuItem);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the menu DOM.
|
||||
* @param {!Element} container Element upon which to append this menu.
|
||||
*/
|
||||
Menu.prototype.render = function(container) {
|
||||
const element =
|
||||
/** @type {!HTMLDivElement} */ (document.createElement('div'));
|
||||
// goog-menu is deprecated, use blocklyMenu. May 2020.
|
||||
element.className = 'blocklyMenu goog-menu blocklyNonSelectable';
|
||||
element.tabIndex = 0;
|
||||
if (this.roleName_) {
|
||||
aria.setRole(element, this.roleName_);
|
||||
}
|
||||
this.element_ = element;
|
||||
|
||||
// Add menu items.
|
||||
for (let i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
|
||||
element.appendChild(menuItem.createDom());
|
||||
}
|
||||
|
||||
// Add event handlers.
|
||||
this.mouseOverHandler_ = browserEvents.conditionalBind(
|
||||
element, 'mouseover', this, this.handleMouseOver_, true);
|
||||
this.clickHandler_ = browserEvents.conditionalBind(
|
||||
element, 'click', this, this.handleClick_, true);
|
||||
this.mouseEnterHandler_ = browserEvents.conditionalBind(
|
||||
element, 'mouseenter', this, this.handleMouseEnter_, true);
|
||||
this.mouseLeaveHandler_ = browserEvents.conditionalBind(
|
||||
element, 'mouseleave', this, this.handleMouseLeave_, true);
|
||||
this.onKeyDownHandler_ = browserEvents.conditionalBind(
|
||||
element, 'keydown', this, this.handleKeyEvent_);
|
||||
|
||||
container.appendChild(element);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the menu's element.
|
||||
* @return {?Element} The DOM element.
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.getElement = function() {
|
||||
return this.element_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the menu element.
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.focus = function() {
|
||||
const el = this.getElement();
|
||||
if (el) {
|
||||
el.focus({preventScroll: true});
|
||||
dom.addClass(el, 'blocklyFocused');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Blur the menu element.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.blur_ = function() {
|
||||
const el = this.getElement();
|
||||
if (el) {
|
||||
el.blur();
|
||||
dom.removeClass(el, 'blocklyFocused');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the menu accessibility role.
|
||||
* @param {!aria.Role} roleName role name.
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.setRole = function(roleName) {
|
||||
this.roleName_ = roleName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispose of this menu.
|
||||
*/
|
||||
Menu.prototype.dispose = function() {
|
||||
// Remove event handlers.
|
||||
if (this.mouseOverHandler_) {
|
||||
browserEvents.unbind(this.mouseOverHandler_);
|
||||
this.mouseOverHandler_ = null;
|
||||
}
|
||||
if (this.clickHandler_) {
|
||||
browserEvents.unbind(this.clickHandler_);
|
||||
this.clickHandler_ = null;
|
||||
}
|
||||
if (this.mouseEnterHandler_) {
|
||||
browserEvents.unbind(this.mouseEnterHandler_);
|
||||
this.mouseEnterHandler_ = null;
|
||||
}
|
||||
if (this.mouseLeaveHandler_) {
|
||||
browserEvents.unbind(this.mouseLeaveHandler_);
|
||||
this.mouseLeaveHandler_ = null;
|
||||
}
|
||||
if (this.onKeyDownHandler_) {
|
||||
browserEvents.unbind(this.onKeyDownHandler_);
|
||||
this.onKeyDownHandler_ = null;
|
||||
}
|
||||
|
||||
// Remove menu items.
|
||||
for (let i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
|
||||
menuItem.dispose();
|
||||
}
|
||||
this.element_ = null;
|
||||
};
|
||||
|
||||
// Child component management.
|
||||
|
||||
/**
|
||||
* 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 {?MenuItem} Menu item for which the DOM element belongs to.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.getMenuItem_ = function(elem) {
|
||||
const 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 (dom.hasClass(elem, 'blocklyMenuItem')) {
|
||||
// Having found a menu item's div, locate that menu item in this menu.
|
||||
for (let i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
|
||||
if (menuItem.getElement() === elem) {
|
||||
return menuItem;
|
||||
getMenuItem_(elem) {
|
||||
const 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 (dom.hasClass(elem, 'blocklyMenuItem')) {
|
||||
// Having found a menu item's div, locate that menu item in this menu.
|
||||
for (let i = 0, menuItem; (menuItem = this.menuItems_[i]); i++) {
|
||||
if (menuItem.getElement() === elem) {
|
||||
return menuItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
elem = elem.parentElement;
|
||||
}
|
||||
elem = elem.parentElement;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Highlight management.
|
||||
// Highlight management.
|
||||
|
||||
/**
|
||||
* Highlights the given menu item, or clears highlighting if null.
|
||||
* @param {?MenuItem} item Item to highlight, or null.
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.setHighlighted = function(item) {
|
||||
const currentHighlighted = this.highlightedItem_;
|
||||
if (currentHighlighted) {
|
||||
currentHighlighted.setHighlighted(false);
|
||||
this.highlightedItem_ = null;
|
||||
}
|
||||
if (item) {
|
||||
item.setHighlighted(true);
|
||||
this.highlightedItem_ = item;
|
||||
// Bring the highlighted item into view. This has no effect if the menu is
|
||||
// not scrollable.
|
||||
const el = /** @type {!Element} */ (this.getElement());
|
||||
style.scrollIntoContainerView(
|
||||
/** @type {!Element} */ (item.getElement()), el);
|
||||
|
||||
aria.setState(el, aria.State.ACTIVEDESCENDANT, item.getId());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights the next highlightable item (or the first if nothing is currently
|
||||
* highlighted).
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.highlightNext = function() {
|
||||
const index = this.menuItems_.indexOf(this.highlightedItem_);
|
||||
this.highlightHelper_(index, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights the previous highlightable item (or the last if nothing is
|
||||
* currently highlighted).
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.highlightPrevious = function() {
|
||||
const index = this.menuItems_.indexOf(this.highlightedItem_);
|
||||
this.highlightHelper_(index < 0 ? this.menuItems_.length : index, -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights the first highlightable item.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.highlightFirst_ = function() {
|
||||
this.highlightHelper_(-1, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights the last highlightable item.
|
||||
* @private
|
||||
*/
|
||||
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 {number} startIndex Start index.
|
||||
* @param {number} delta Step direction: 1 to go down, -1 to go up.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.highlightHelper_ = function(startIndex, delta) {
|
||||
let index = startIndex + delta;
|
||||
let menuItem;
|
||||
while ((menuItem = this.menuItems_[index])) {
|
||||
if (menuItem.isEnabled()) {
|
||||
this.setHighlighted(menuItem);
|
||||
break;
|
||||
/**
|
||||
* Highlights the given menu item, or clears highlighting if null.
|
||||
* @param {?MenuItem} item Item to highlight, or null.
|
||||
* @package
|
||||
*/
|
||||
setHighlighted(item) {
|
||||
const currentHighlighted = this.highlightedItem_;
|
||||
if (currentHighlighted) {
|
||||
currentHighlighted.setHighlighted(false);
|
||||
this.highlightedItem_ = null;
|
||||
}
|
||||
if (item) {
|
||||
item.setHighlighted(true);
|
||||
this.highlightedItem_ = item;
|
||||
// Bring the highlighted item into view. This has no effect if the menu is
|
||||
// not scrollable.
|
||||
const el = /** @type {!Element} */ (this.getElement());
|
||||
style.scrollIntoContainerView(
|
||||
/** @type {!Element} */ (item.getElement()), el);
|
||||
|
||||
aria.setState(el, aria.State.ACTIVEDESCENDANT, item.getId());
|
||||
}
|
||||
index += delta;
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse events.
|
||||
/**
|
||||
* Highlights the next highlightable item (or the first if nothing is
|
||||
* currently highlighted).
|
||||
* @package
|
||||
*/
|
||||
highlightNext() {
|
||||
const index = this.menuItems_.indexOf(this.highlightedItem_);
|
||||
this.highlightHelper_(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouseover events. Highlight menuitems as the user hovers over them.
|
||||
* @param {!Event} e Mouse event to handle.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.handleMouseOver_ = function(e) {
|
||||
const menuItem = this.getMenuItem_(/** @type {Element} */ (e.target));
|
||||
/**
|
||||
* Highlights the previous highlightable item (or the last if nothing is
|
||||
* currently highlighted).
|
||||
* @package
|
||||
*/
|
||||
highlightPrevious() {
|
||||
const index = this.menuItems_.indexOf(this.highlightedItem_);
|
||||
this.highlightHelper_(index < 0 ? this.menuItems_.length : index, -1);
|
||||
}
|
||||
|
||||
if (menuItem) {
|
||||
if (menuItem.isEnabled()) {
|
||||
if (this.highlightedItem_ !== menuItem) {
|
||||
/**
|
||||
* Highlights the first highlightable item.
|
||||
* @private
|
||||
*/
|
||||
highlightFirst_() {
|
||||
this.highlightHelper_(-1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the last highlightable item.
|
||||
* @private
|
||||
*/
|
||||
highlightLast_() {
|
||||
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 {number} startIndex Start index.
|
||||
* @param {number} delta Step direction: 1 to go down, -1 to go up.
|
||||
* @private
|
||||
*/
|
||||
highlightHelper_(startIndex, delta) {
|
||||
let index = startIndex + delta;
|
||||
let menuItem;
|
||||
while ((menuItem = this.menuItems_[index])) {
|
||||
if (menuItem.isEnabled()) {
|
||||
this.setHighlighted(menuItem);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
index += delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse events.
|
||||
|
||||
/**
|
||||
* Handles mouseover events. Highlight menuitems as the user hovers over them.
|
||||
* @param {!Event} e Mouse event to handle.
|
||||
* @private
|
||||
*/
|
||||
handleMouseOver_(e) {
|
||||
const menuItem = this.getMenuItem_(/** @type {Element} */ (e.target));
|
||||
|
||||
if (menuItem) {
|
||||
if (menuItem.isEnabled()) {
|
||||
if (this.highlightedItem_ !== menuItem) {
|
||||
this.setHighlighted(menuItem);
|
||||
}
|
||||
} else {
|
||||
this.setHighlighted(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click events. Pass the event onto the child menuitem to handle.
|
||||
* @param {!Event} e Click event to handle.
|
||||
* @private
|
||||
*/
|
||||
handleClick_(e) {
|
||||
const 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') {
|
||||
const newCoords = new Coordinate(e.clientX, e.clientY);
|
||||
if (Coordinate.distance(oldCoords, newCoords) < 1) {
|
||||
// This menu was opened by a mousedown and we're handling the consequent
|
||||
// click event. The coords haven't changed, meaning this was the same
|
||||
// opening event. Don't do the usual behavior because the menu just
|
||||
// popped up under the mouse and the user didn't mean to activate this
|
||||
// item.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const menuItem = this.getMenuItem_(/** @type {Element} */ (e.target));
|
||||
if (menuItem) {
|
||||
menuItem.performAction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse enter events. Focus the element.
|
||||
* @param {!Event} _e Mouse event to handle.
|
||||
* @private
|
||||
*/
|
||||
handleMouseEnter_(_e) {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse leave events. Blur and clear highlight.
|
||||
* @param {!Event} _e Mouse event to handle.
|
||||
* @private
|
||||
*/
|
||||
handleMouseLeave_(_e) {
|
||||
if (this.getElement()) {
|
||||
this.blur_();
|
||||
this.setHighlighted(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles click events. Pass the event onto the child menuitem to handle.
|
||||
* @param {!Event} e Click event to handle.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.handleClick_ = function(e) {
|
||||
const 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') {
|
||||
const newCoords = new Coordinate(e.clientX, e.clientY);
|
||||
if (Coordinate.distance(oldCoords, newCoords) < 1) {
|
||||
// This menu was opened by a mousedown and we're handling the consequent
|
||||
// click event. The coords haven't changed, meaning this was the same
|
||||
// opening event. Don't do the usual behavior because the menu just popped
|
||||
// up under the mouse and the user didn't mean to activate this item.
|
||||
// Keyboard events.
|
||||
|
||||
/**
|
||||
* Attempts to handle a keyboard event, if the menu item is enabled, by
|
||||
* calling
|
||||
* {@link handleKeyEventInternal_}.
|
||||
* @param {!Event} e Key event to handle.
|
||||
* @private
|
||||
*/
|
||||
handleKeyEvent_(e) {
|
||||
if (!this.menuItems_.length) {
|
||||
// Empty menu.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const menuItem = this.getMenuItem_(/** @type {Element} */ (e.target));
|
||||
if (menuItem) {
|
||||
menuItem.performAction();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles mouse enter events. Focus the element.
|
||||
* @param {!Event} _e Mouse event to handle.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.handleMouseEnter_ = function(_e) {
|
||||
this.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles mouse leave events. Blur and clear highlight.
|
||||
* @param {!Event} _e Mouse event to handle.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.handleMouseLeave_ = function(_e) {
|
||||
if (this.getElement()) {
|
||||
this.blur_();
|
||||
this.setHighlighted(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard events.
|
||||
|
||||
/**
|
||||
* Attempts to handle a keyboard event, if the menu item is enabled, by calling
|
||||
* {@link handleKeyEventInternal_}.
|
||||
* @param {!Event} e Key event to handle.
|
||||
* @private
|
||||
*/
|
||||
Menu.prototype.handleKeyEvent_ = function(e) {
|
||||
if (!this.menuItems_.length) {
|
||||
// Empty menu.
|
||||
return;
|
||||
}
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
|
||||
// Do not handle the key event if any modifier key is pressed.
|
||||
return;
|
||||
}
|
||||
|
||||
const highlighted = this.highlightedItem_;
|
||||
switch (e.keyCode) {
|
||||
case KeyCodes.ENTER:
|
||||
case KeyCodes.SPACE:
|
||||
if (highlighted) {
|
||||
highlighted.performAction();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCodes.UP:
|
||||
this.highlightPrevious();
|
||||
break;
|
||||
|
||||
case KeyCodes.DOWN:
|
||||
this.highlightNext();
|
||||
break;
|
||||
|
||||
case KeyCodes.PAGE_UP:
|
||||
case KeyCodes.HOME:
|
||||
this.highlightFirst_();
|
||||
break;
|
||||
|
||||
case KeyCodes.PAGE_DOWN:
|
||||
case KeyCodes.END:
|
||||
this.highlightLast_();
|
||||
break;
|
||||
|
||||
default:
|
||||
// Not a key the menu is interested in.
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) {
|
||||
// Do not handle the key event if any modifier key is pressed.
|
||||
return;
|
||||
}
|
||||
// The menu used this key, don't let it have secondary effects.
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a rendered menu.
|
||||
* @return {!Size} Object with width and height properties.
|
||||
* @package
|
||||
*/
|
||||
Menu.prototype.getSize = function() {
|
||||
const menuDom = this.getElement();
|
||||
const menuSize = style.getSize(/** @type {!Element} */
|
||||
(menuDom));
|
||||
// Recalculate height for the total content, not only box height.
|
||||
menuSize.height = menuDom.scrollHeight;
|
||||
return menuSize;
|
||||
const highlighted = this.highlightedItem_;
|
||||
switch (e.keyCode) {
|
||||
case KeyCodes.ENTER:
|
||||
case KeyCodes.SPACE:
|
||||
if (highlighted) {
|
||||
highlighted.performAction();
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyCodes.UP:
|
||||
this.highlightPrevious();
|
||||
break;
|
||||
|
||||
case KeyCodes.DOWN:
|
||||
this.highlightNext();
|
||||
break;
|
||||
|
||||
case KeyCodes.PAGE_UP:
|
||||
case KeyCodes.HOME:
|
||||
this.highlightFirst_();
|
||||
break;
|
||||
|
||||
case KeyCodes.PAGE_DOWN:
|
||||
case 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a rendered menu.
|
||||
* @return {!Size} Object with width and height properties.
|
||||
* @package
|
||||
*/
|
||||
getSize() {
|
||||
const menuDom = this.getElement();
|
||||
const menuSize = style.getSize(/** @type {!Element} */
|
||||
(menuDom));
|
||||
// Recalculate height for the total content, not only box height.
|
||||
menuSize.height = menuDom.scrollHeight;
|
||||
return menuSize;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Menu = Menu;
|
||||
|
||||
459
core/menuitem.js
459
core/menuitem.js
@@ -22,265 +22,268 @@ const idGenerator = goog.require('Blockly.utils.idGenerator');
|
||||
|
||||
/**
|
||||
* Class representing an item in a menu.
|
||||
*
|
||||
* @param {string|!HTMLElement} content Text caption to display as the content
|
||||
* of the item, or a HTML element to display.
|
||||
* @param {string=} opt_value Data/model associated with the menu item.
|
||||
* @constructor
|
||||
* @alias Blockly.MenuItem
|
||||
*/
|
||||
const MenuItem = function(content, opt_value) {
|
||||
const MenuItem = class {
|
||||
/**
|
||||
* Human-readable text of this menu item, or the HTML element to display.
|
||||
* @type {string|!HTMLElement}
|
||||
* @private
|
||||
* @param {string|!HTMLElement} content Text caption to display as the content
|
||||
* of the item, or a HTML element to display.
|
||||
* @param {string=} opt_value Data/model associated with the menu item.
|
||||
* @alias Blockly.MenuItem
|
||||
*/
|
||||
this.content_ = content;
|
||||
constructor(content, opt_value) {
|
||||
/**
|
||||
* Human-readable text of this menu item, or the HTML element to display.
|
||||
* @type {string|!HTMLElement}
|
||||
* @private
|
||||
*/
|
||||
this.content_ = content;
|
||||
|
||||
/**
|
||||
* Machine-readable value of this menu item.
|
||||
* @type {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
this.value_ = opt_value;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Is the menu item clickable, as opposed to greyed-out.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.enabled_ = true;
|
||||
|
||||
/**
|
||||
* The DOM element for the menu item.
|
||||
* @type {?Element}
|
||||
* @private
|
||||
*/
|
||||
this.element_ = null;
|
||||
/**
|
||||
* The DOM element for the menu item.
|
||||
* @type {?Element}
|
||||
* @private
|
||||
*/
|
||||
this.element_ = null;
|
||||
|
||||
/**
|
||||
* Whether the menu item is rendered right-to-left.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.rightToLeft_ = false;
|
||||
/**
|
||||
* Whether the menu item is rendered right-to-left.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.rightToLeft_ = false;
|
||||
|
||||
/**
|
||||
* ARIA name for this menu.
|
||||
* @type {?aria.Role}
|
||||
* @private
|
||||
*/
|
||||
this.roleName_ = null;
|
||||
/**
|
||||
* ARIA name for this menu.
|
||||
* @type {?aria.Role}
|
||||
* @private
|
||||
*/
|
||||
this.roleName_ = null;
|
||||
|
||||
/**
|
||||
* Is this menu item checkable.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.checkable_ = false;
|
||||
/**
|
||||
* Is this menu item checkable.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.checkable_ = false;
|
||||
|
||||
/**
|
||||
* Is this menu item currently checked.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.checked_ = false;
|
||||
/**
|
||||
* Is this menu item currently checked.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.checked_ = false;
|
||||
|
||||
/**
|
||||
* Is this menu item currently highlighted.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.highlight_ = false;
|
||||
/**
|
||||
* Is this menu item currently highlighted.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.highlight_ = false;
|
||||
|
||||
/**
|
||||
* Bound function to call when this menu item is clicked.
|
||||
* @type {?Function}
|
||||
* @private
|
||||
*/
|
||||
this.actionHandler_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates the menuitem's DOM.
|
||||
* @return {!Element} Completed DOM.
|
||||
*/
|
||||
MenuItem.prototype.createDom = function() {
|
||||
const element = document.createElement('div');
|
||||
element.id = idGenerator.getNextUniqueId();
|
||||
this.element_ = element;
|
||||
|
||||
// Set class and style
|
||||
// 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.highlight_ ? 'blocklyMenuItemHighlight goog-menuitem-highlight ' :
|
||||
'') +
|
||||
(this.rightToLeft_ ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : '');
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'blocklyMenuItemContent goog-menuitem-content';
|
||||
// Add a checkbox for checkable menu items.
|
||||
if (this.checkable_) {
|
||||
const checkbox = document.createElement('div');
|
||||
checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox';
|
||||
content.appendChild(checkbox);
|
||||
/**
|
||||
* Bound function to call when this menu item is clicked.
|
||||
* @type {?Function}
|
||||
* @private
|
||||
*/
|
||||
this.actionHandler_ = null;
|
||||
}
|
||||
|
||||
let contentDom = /** @type {!HTMLElement} */ (this.content_);
|
||||
if (typeof this.content_ === 'string') {
|
||||
contentDom = document.createTextNode(this.content_);
|
||||
/**
|
||||
* Creates the menuitem's DOM.
|
||||
* @return {!Element} Completed DOM.
|
||||
*/
|
||||
createDom() {
|
||||
const element = document.createElement('div');
|
||||
element.id = idGenerator.getNextUniqueId();
|
||||
this.element_ = element;
|
||||
|
||||
// Set class and style
|
||||
// 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.highlight_ ? 'blocklyMenuItemHighlight goog-menuitem-highlight ' :
|
||||
'') +
|
||||
(this.rightToLeft_ ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : '');
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'blocklyMenuItemContent goog-menuitem-content';
|
||||
// Add a checkbox for checkable menu items.
|
||||
if (this.checkable_) {
|
||||
const checkbox = document.createElement('div');
|
||||
checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox';
|
||||
content.appendChild(checkbox);
|
||||
}
|
||||
|
||||
let contentDom = /** @type {!HTMLElement} */ (this.content_);
|
||||
if (typeof this.content_ === 'string') {
|
||||
contentDom = document.createTextNode(this.content_);
|
||||
}
|
||||
content.appendChild(contentDom);
|
||||
element.appendChild(content);
|
||||
|
||||
// Initialize ARIA role and state.
|
||||
if (this.roleName_) {
|
||||
aria.setRole(element, this.roleName_);
|
||||
}
|
||||
aria.setState(
|
||||
element, aria.State.SELECTED,
|
||||
(this.checkable_ && this.checked_) || false);
|
||||
aria.setState(element, aria.State.DISABLED, !this.enabled_);
|
||||
|
||||
return element;
|
||||
}
|
||||
content.appendChild(contentDom);
|
||||
element.appendChild(content);
|
||||
|
||||
// Initialize ARIA role and state.
|
||||
if (this.roleName_) {
|
||||
aria.setRole(element, this.roleName_);
|
||||
/**
|
||||
* Dispose of this menu item.
|
||||
*/
|
||||
dispose() {
|
||||
this.element_ = null;
|
||||
}
|
||||
aria.setState(
|
||||
element, aria.State.SELECTED,
|
||||
(this.checkable_ && this.checked_) || false);
|
||||
aria.setState(element, aria.State.DISABLED, !this.enabled_);
|
||||
|
||||
return element;
|
||||
};
|
||||
/**
|
||||
* Gets the menu item's element.
|
||||
* @return {?Element} The DOM element.
|
||||
* @package
|
||||
*/
|
||||
getElement() {
|
||||
return this.element_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of this menu item.
|
||||
*/
|
||||
MenuItem.prototype.dispose = function() {
|
||||
this.element_ = null;
|
||||
};
|
||||
/**
|
||||
* Gets the unique ID for this menu item.
|
||||
* @return {string} Unique component ID.
|
||||
* @package
|
||||
*/
|
||||
getId() {
|
||||
return this.element_.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the menu item's element.
|
||||
* @return {?Element} The DOM element.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.getElement = function() {
|
||||
return this.element_;
|
||||
};
|
||||
/**
|
||||
* Gets the value associated with the menu item.
|
||||
* @return {*} value Value associated with the menu item.
|
||||
* @package
|
||||
*/
|
||||
getValue() {
|
||||
return this.value_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique ID for this menu item.
|
||||
* @return {string} Unique component ID.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.getId = function() {
|
||||
return this.element_.id;
|
||||
};
|
||||
/**
|
||||
* Set menu item's rendering direction.
|
||||
* @param {boolean} rtl True if RTL, false if LTR.
|
||||
* @package
|
||||
*/
|
||||
setRightToLeft(rtl) {
|
||||
this.rightToLeft_ = rtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value associated with the menu item.
|
||||
* @return {*} value Value associated with the menu item.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.getValue = function() {
|
||||
return this.value_;
|
||||
};
|
||||
/**
|
||||
* Set the menu item's accessibility role.
|
||||
* @param {!aria.Role} roleName Role name.
|
||||
* @package
|
||||
*/
|
||||
setRole(roleName) {
|
||||
this.roleName_ = roleName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set menu item's rendering direction.
|
||||
* @param {boolean} rtl True if RTL, false if LTR.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.setRightToLeft = function(rtl) {
|
||||
this.rightToLeft_ = rtl;
|
||||
};
|
||||
/**
|
||||
* Sets the menu item to be checkable or not. Set to true for menu items
|
||||
* that represent checkable options.
|
||||
* @param {boolean} checkable Whether the menu item is checkable.
|
||||
* @package
|
||||
*/
|
||||
setCheckable(checkable) {
|
||||
this.checkable_ = checkable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the menu item's accessibility role.
|
||||
* @param {!aria.Role} roleName Role name.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.setRole = function(roleName) {
|
||||
this.roleName_ = roleName;
|
||||
};
|
||||
/**
|
||||
* Checks or unchecks the component.
|
||||
* @param {boolean} checked Whether to check or uncheck the component.
|
||||
* @package
|
||||
*/
|
||||
setChecked(checked) {
|
||||
this.checked_ = checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the menu item to be checkable or not. Set to true for menu items
|
||||
* that represent checkable options.
|
||||
* @param {boolean} checkable Whether the menu item is checkable.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.setCheckable = function(checkable) {
|
||||
this.checkable_ = checkable;
|
||||
};
|
||||
/**
|
||||
* Highlights or unhighlights the component.
|
||||
* @param {boolean} highlight Whether to highlight or unhighlight the
|
||||
* component.
|
||||
* @package
|
||||
*/
|
||||
setHighlighted(highlight) {
|
||||
this.highlight_ = highlight;
|
||||
|
||||
/**
|
||||
* Checks or unchecks the component.
|
||||
* @param {boolean} checked Whether to check or uncheck the component.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.setChecked = function(checked) {
|
||||
this.checked_ = checked;
|
||||
};
|
||||
|
||||
/**
|
||||
* Highlights or unhighlights the component.
|
||||
* @param {boolean} highlight Whether to highlight or unhighlight the component.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.setHighlighted = function(highlight) {
|
||||
this.highlight_ = highlight;
|
||||
|
||||
const el = this.getElement();
|
||||
if (el && this.isEnabled()) {
|
||||
// goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight.
|
||||
// May 2020.
|
||||
const name = 'blocklyMenuItemHighlight';
|
||||
const nameDep = 'goog-menuitem-highlight';
|
||||
if (highlight) {
|
||||
dom.addClass(el, name);
|
||||
dom.addClass(el, nameDep);
|
||||
} else {
|
||||
dom.removeClass(el, name);
|
||||
dom.removeClass(el, nameDep);
|
||||
const el = this.getElement();
|
||||
if (el && this.isEnabled()) {
|
||||
// goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight.
|
||||
// May 2020.
|
||||
const name = 'blocklyMenuItemHighlight';
|
||||
const nameDep = 'goog-menuitem-highlight';
|
||||
if (highlight) {
|
||||
dom.addClass(el, name);
|
||||
dom.addClass(el, nameDep);
|
||||
} else {
|
||||
dom.removeClass(el, name);
|
||||
dom.removeClass(el, nameDep);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the menu item is enabled, false otherwise.
|
||||
* @return {boolean} Whether the menu item is enabled.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.isEnabled = function() {
|
||||
return this.enabled_;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enables or disables the menu item.
|
||||
* @param {boolean} enabled Whether to enable or disable the menu item.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.setEnabled = function(enabled) {
|
||||
this.enabled_ = enabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs the appropriate action when the menu item is activated
|
||||
* by the user.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.performAction = function() {
|
||||
if (this.isEnabled() && this.actionHandler_) {
|
||||
this.actionHandler_(this);
|
||||
/**
|
||||
* Returns true if the menu item is enabled, false otherwise.
|
||||
* @return {boolean} Whether the menu item is enabled.
|
||||
* @package
|
||||
*/
|
||||
isEnabled() {
|
||||
return this.enabled_;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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(!MenuItem)} fn The handler.
|
||||
* @param {!Object} obj Used as the 'this' object in fn when called.
|
||||
* @package
|
||||
*/
|
||||
MenuItem.prototype.onAction = function(fn, obj) {
|
||||
this.actionHandler_ = fn.bind(obj);
|
||||
/**
|
||||
* Enables or disables the menu item.
|
||||
* @param {boolean} enabled Whether to enable or disable the menu item.
|
||||
* @package
|
||||
*/
|
||||
setEnabled(enabled) {
|
||||
this.enabled_ = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the appropriate action when the menu item is activated
|
||||
* by the user.
|
||||
* @package
|
||||
*/
|
||||
performAction() {
|
||||
if (this.isEnabled() && this.actionHandler_) {
|
||||
this.actionHandler_(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(!MenuItem)} fn The handler.
|
||||
* @param {!Object} obj Used as the 'this' object in fn when called.
|
||||
* @package
|
||||
*/
|
||||
onAction(fn, obj) {
|
||||
this.actionHandler_ = fn.bind(obj);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MenuItem = MenuItem;
|
||||
|
||||
415
core/names.js
415
core/names.js
@@ -27,23 +27,235 @@ goog.requireType('Blockly.Procedures');
|
||||
|
||||
/**
|
||||
* Class for a database of entity names (variables, procedures, etc).
|
||||
* @param {string} reservedWords A comma-separated string of words that are
|
||||
* illegal for use as names in a language (e.g. 'new,if,this,...').
|
||||
* @param {string=} opt_variablePrefix Some languages need a '$' or a namespace
|
||||
* before all variable names (but not procedure names).
|
||||
* @constructor
|
||||
* @alias Blockly.Names
|
||||
*/
|
||||
const Names = function(reservedWords, opt_variablePrefix) {
|
||||
this.variablePrefix_ = opt_variablePrefix || '';
|
||||
this.reservedDict_ = Object.create(null);
|
||||
if (reservedWords) {
|
||||
const splitWords = reservedWords.split(',');
|
||||
for (let i = 0; i < splitWords.length; i++) {
|
||||
this.reservedDict_[splitWords[i]] = true;
|
||||
const Names = class {
|
||||
/**
|
||||
* @param {string} reservedWords A comma-separated string of words that are
|
||||
* illegal for use as names in a language (e.g. 'new,if,this,...').
|
||||
* @param {string=} opt_variablePrefix Some languages need a '$' or a
|
||||
* namespace before all variable names (but not procedure names).
|
||||
* @alias Blockly.Names
|
||||
*/
|
||||
constructor(reservedWords, opt_variablePrefix) {
|
||||
/**
|
||||
* The prefix to attach to variable names in generated code.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.variablePrefix_ = opt_variablePrefix || '';
|
||||
|
||||
/**
|
||||
* A dictionary of reserved words.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this.reservedDict_ = Object.create(null);
|
||||
|
||||
/**
|
||||
* A map from type (e.g. name, procedure) to maps from names to generated
|
||||
* names.
|
||||
* @type {Object<string, Object<string, string>>}
|
||||
* @private
|
||||
*/
|
||||
this.db_ = Object.create(null);
|
||||
|
||||
/**
|
||||
* A map from used names to booleans to avoid collisions.
|
||||
* @type {Object<string, boolean>}
|
||||
* @private
|
||||
*/
|
||||
this.dbReverse_ = Object.create(null);
|
||||
|
||||
/**
|
||||
* The variable map from the workspace, containing Blockly variable models.
|
||||
* @type {?VariableMap}
|
||||
* @private
|
||||
*/
|
||||
this.variableMap_ = null;
|
||||
|
||||
if (reservedWords) {
|
||||
const splitWords = reservedWords.split(',');
|
||||
for (let i = 0; i < splitWords.length; i++) {
|
||||
this.reservedDict_[splitWords[i]] = true;
|
||||
}
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty the database and start from scratch. The reserved words are kept.
|
||||
*/
|
||||
reset() {
|
||||
this.db_ = Object.create(null);
|
||||
this.dbReverse_ = Object.create(null);
|
||||
this.variableMap_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the variable map that maps from variable name to variable object.
|
||||
* @param {!VariableMap} map The map to track.
|
||||
*/
|
||||
setVariableMap(map) {
|
||||
this.variableMap_ = map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name for a user-defined variable, based on its ID.
|
||||
* This should only be used for variables of NameType VARIABLE.
|
||||
* @param {string} id The ID to look up in the variable map.
|
||||
* @return {?string} The name of the referenced variable, or null if there was
|
||||
* no variable map or the variable was not found in the map.
|
||||
* @private
|
||||
*/
|
||||
getNameForUserVariable_(id) {
|
||||
if (!this.variableMap_) {
|
||||
console.warn(
|
||||
'Deprecated call to Names.prototype.getName without ' +
|
||||
'defining a variable map. To fix, add the following code in your ' +
|
||||
'generator\'s init() function:\n' +
|
||||
'Blockly.YourGeneratorName.nameDB_.setVariableMap(' +
|
||||
'workspace.getVariableMap());');
|
||||
return null;
|
||||
}
|
||||
const variable = this.variableMap_.getVariableById(id);
|
||||
if (variable) {
|
||||
return variable.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate names for user variables, but only ones that are being used.
|
||||
* @param {!Workspace} workspace Workspace to generate variables from.
|
||||
*/
|
||||
populateVariables(workspace) {
|
||||
const variables = Variables.allUsedVarModels(workspace);
|
||||
for (let i = 0; i < variables.length; i++) {
|
||||
this.getName(variables[i].getId(), NameType.VARIABLE);
|
||||
}
|
||||
}
|
||||
this.reset();
|
||||
|
||||
/**
|
||||
* Generate names for procedures.
|
||||
* @param {!Workspace} workspace Workspace to generate procedures from.
|
||||
*/
|
||||
populateProcedures(workspace) {
|
||||
let procedures =
|
||||
goog.module.get('Blockly.Procedures').allProcedures(workspace);
|
||||
// Flatten the return vs no-return procedure lists.
|
||||
procedures = procedures[0].concat(procedures[1]);
|
||||
for (let i = 0; i < procedures.length; i++) {
|
||||
this.getName(procedures[i][0], NameType.PROCEDURE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Blockly entity name to a legal exportable entity name.
|
||||
* @param {string} nameOrId The Blockly entity name (no constraints) or
|
||||
* variable ID.
|
||||
* @param {NameType|string} type The type of the name in Blockly
|
||||
* ('VARIABLE', 'PROCEDURE', 'DEVELOPER_VARIABLE', etc...).
|
||||
* @return {string} An entity name that is legal in the exported language.
|
||||
*/
|
||||
getName(nameOrId, type) {
|
||||
let name = nameOrId;
|
||||
if (type === NameType.VARIABLE) {
|
||||
const varName = this.getNameForUserVariable_(nameOrId);
|
||||
if (varName) {
|
||||
// Successful ID lookup.
|
||||
name = varName;
|
||||
}
|
||||
}
|
||||
const normalizedName = name.toLowerCase();
|
||||
|
||||
const isVar =
|
||||
type === NameType.VARIABLE || type === NameType.DEVELOPER_VARIABLE;
|
||||
|
||||
const prefix = isVar ? this.variablePrefix_ : '';
|
||||
if (!(type in this.db_)) {
|
||||
this.db_[type] = Object.create(null);
|
||||
}
|
||||
const typeDb = this.db_[type];
|
||||
if (normalizedName in typeDb) {
|
||||
return prefix + typeDb[normalizedName];
|
||||
}
|
||||
const safeName = this.getDistinctName(name, type);
|
||||
typeDb[normalizedName] = safeName.substr(prefix.length);
|
||||
return safeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all known user-created names of a specified name type.
|
||||
* @param {NameType|string} type The type of entity in Blockly
|
||||
* ('VARIABLE', 'PROCEDURE', 'DEVELOPER_VARIABLE', etc...).
|
||||
* @return {!Array<string>} A list of Blockly entity names (no constraints).
|
||||
*/
|
||||
getUserNames(type) {
|
||||
const typeDb = this.db_[type] || {};
|
||||
return Object.keys(typeDb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Blockly entity name to a legal exportable entity name.
|
||||
* Ensure that this is a new name not overlapping any previously defined name.
|
||||
* Also check against list of reserved words for the current language and
|
||||
* ensure name doesn't collide.
|
||||
* @param {string} name The Blockly entity name (no constraints).
|
||||
* @param {NameType|string} type The type of entity in Blockly
|
||||
* ('VARIABLE', 'PROCEDURE', 'DEVELOPER_VARIABLE', etc...).
|
||||
* @return {string} An entity name that is legal in the exported language.
|
||||
*/
|
||||
getDistinctName(name, type) {
|
||||
let safeName = this.safeName_(name);
|
||||
let i = '';
|
||||
while (this.dbReverse_[safeName + i] ||
|
||||
(safeName + i) in this.reservedDict_) {
|
||||
// Collision with existing name. Create a unique name.
|
||||
i = i ? i + 1 : 2;
|
||||
}
|
||||
safeName += i;
|
||||
this.dbReverse_[safeName] = true;
|
||||
const isVar =
|
||||
type === NameType.VARIABLE || type === NameType.DEVELOPER_VARIABLE;
|
||||
const prefix = isVar ? this.variablePrefix_ : '';
|
||||
return prefix + safeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a proposed entity name, generate a name that conforms to the
|
||||
* [_A-Za-z][_A-Za-z0-9]* format that most languages consider legal for
|
||||
* variable and function names.
|
||||
* @param {string} name Potentially illegal entity name.
|
||||
* @return {string} Safe entity name.
|
||||
* @private
|
||||
*/
|
||||
safeName_(name) {
|
||||
if (!name) {
|
||||
name = Msg['UNNAMED_KEY'] || 'unnamed';
|
||||
} else {
|
||||
// Unfortunately names in non-latin characters will look like
|
||||
// _E9_9F_B3_E4_B9_90 which is pretty meaningless.
|
||||
// https://github.com/google/blockly/issues/1654
|
||||
name = encodeURI(name.replace(/ /g, '_')).replace(/[^\w]/g, '_');
|
||||
// Most languages don't allow names with leading numbers.
|
||||
if ('0123456789'.indexOf(name[0]) !== -1) {
|
||||
name = 'my_' + name;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the given two entity names refer to the same entity?
|
||||
* Blockly names are case-insensitive.
|
||||
* @param {string} name1 First name.
|
||||
* @param {string} name2 Second name.
|
||||
* @return {boolean} True if names are the same.
|
||||
*/
|
||||
static equals(name1, name2) {
|
||||
// name1.localeCompare(name2) is slower.
|
||||
return name1.toLowerCase() === name2.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,179 +286,4 @@ exports.NameType = NameType;
|
||||
*/
|
||||
Names.DEVELOPER_VARIABLE_TYPE = NameType.DEVELOPER_VARIABLE;
|
||||
|
||||
/**
|
||||
* Empty the database and start from scratch. The reserved words are kept.
|
||||
*/
|
||||
Names.prototype.reset = function() {
|
||||
this.db_ = Object.create(null);
|
||||
this.dbReverse_ = Object.create(null);
|
||||
this.variableMap_ = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the variable map that maps from variable name to variable object.
|
||||
* @param {!VariableMap} map The map to track.
|
||||
*/
|
||||
Names.prototype.setVariableMap = function(map) {
|
||||
this.variableMap_ = map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the name for a user-defined variable, based on its ID.
|
||||
* This should only be used for variables of NameType VARIABLE.
|
||||
* @param {string} id The ID to look up in the variable map.
|
||||
* @return {?string} The name of the referenced variable, or null if there was
|
||||
* no variable map or the variable was not found in the map.
|
||||
* @private
|
||||
*/
|
||||
Names.prototype.getNameForUserVariable_ = function(id) {
|
||||
if (!this.variableMap_) {
|
||||
console.warn(
|
||||
'Deprecated call to Names.prototype.getName without ' +
|
||||
'defining a variable map. To fix, add the following code in your ' +
|
||||
'generator\'s init() function:\n' +
|
||||
'Blockly.YourGeneratorName.nameDB_.setVariableMap(' +
|
||||
'workspace.getVariableMap());');
|
||||
return null;
|
||||
}
|
||||
const variable = this.variableMap_.getVariableById(id);
|
||||
if (variable) {
|
||||
return variable.name;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate names for user variables, but only ones that are being used.
|
||||
* @param {!Workspace} workspace Workspace to generate variables from.
|
||||
*/
|
||||
Names.prototype.populateVariables = function(workspace) {
|
||||
const variables = Variables.allUsedVarModels(workspace);
|
||||
for (let i = 0; i < variables.length; i++) {
|
||||
this.getName(variables[i].getId(), NameType.VARIABLE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate names for procedures.
|
||||
* @param {!Workspace} workspace Workspace to generate procedures from.
|
||||
*/
|
||||
Names.prototype.populateProcedures = function(workspace) {
|
||||
let procedures =
|
||||
goog.module.get('Blockly.Procedures').allProcedures(workspace);
|
||||
// Flatten the return vs no-return procedure lists.
|
||||
procedures = procedures[0].concat(procedures[1]);
|
||||
for (let i = 0; i < procedures.length; i++) {
|
||||
this.getName(procedures[i][0], NameType.PROCEDURE);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Blockly entity name to a legal exportable entity name.
|
||||
* @param {string} nameOrId The Blockly entity name (no constraints) or
|
||||
* variable ID.
|
||||
* @param {NameType|string} type The type of the name in Blockly
|
||||
* ('VARIABLE', 'PROCEDURE', 'DEVELOPER_VARIABLE', etc...).
|
||||
* @return {string} An entity name that is legal in the exported language.
|
||||
*/
|
||||
Names.prototype.getName = function(nameOrId, type) {
|
||||
let name = nameOrId;
|
||||
if (type === NameType.VARIABLE) {
|
||||
const varName = this.getNameForUserVariable_(nameOrId);
|
||||
if (varName) {
|
||||
// Successful ID lookup.
|
||||
name = varName;
|
||||
}
|
||||
}
|
||||
const normalizedName = name.toLowerCase();
|
||||
|
||||
const isVar =
|
||||
type === NameType.VARIABLE || type === NameType.DEVELOPER_VARIABLE;
|
||||
|
||||
const prefix = isVar ? this.variablePrefix_ : '';
|
||||
if (!(type in this.db_)) {
|
||||
this.db_[type] = Object.create(null);
|
||||
}
|
||||
const typeDb = this.db_[type];
|
||||
if (normalizedName in typeDb) {
|
||||
return prefix + typeDb[normalizedName];
|
||||
}
|
||||
const safeName = this.getDistinctName(name, type);
|
||||
typeDb[normalizedName] = safeName.substr(prefix.length);
|
||||
return safeName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a list of all known user-created names of a specified name type.
|
||||
* @param {NameType|string} type The type of entity in Blockly
|
||||
* ('VARIABLE', 'PROCEDURE', 'DEVELOPER_VARIABLE', etc...).
|
||||
* @return {!Array<string>} A list of Blockly entity names (no constraints).
|
||||
*/
|
||||
Names.prototype.getUserNames = function(type) {
|
||||
const typeDb = this.db_[type] || {};
|
||||
return Object.keys(typeDb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Blockly entity name to a legal exportable entity name.
|
||||
* Ensure that this is a new name not overlapping any previously defined name.
|
||||
* Also check against list of reserved words for the current language and
|
||||
* ensure name doesn't collide.
|
||||
* @param {string} name The Blockly entity name (no constraints).
|
||||
* @param {NameType|string} type The type of entity in Blockly
|
||||
* ('VARIABLE', 'PROCEDURE', 'DEVELOPER_VARIABLE', etc...).
|
||||
* @return {string} An entity name that is legal in the exported language.
|
||||
*/
|
||||
Names.prototype.getDistinctName = function(name, type) {
|
||||
let safeName = this.safeName_(name);
|
||||
let i = '';
|
||||
while (this.dbReverse_[safeName + i] ||
|
||||
(safeName + i) in this.reservedDict_) {
|
||||
// Collision with existing name. Create a unique name.
|
||||
i = i ? i + 1 : 2;
|
||||
}
|
||||
safeName += i;
|
||||
this.dbReverse_[safeName] = true;
|
||||
const isVar =
|
||||
type === NameType.VARIABLE || type === NameType.DEVELOPER_VARIABLE;
|
||||
const prefix = isVar ? this.variablePrefix_ : '';
|
||||
return prefix + safeName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a proposed entity name, generate a name that conforms to the
|
||||
* [_A-Za-z][_A-Za-z0-9]* format that most languages consider legal for
|
||||
* variable and function names.
|
||||
* @param {string} name Potentially illegal entity name.
|
||||
* @return {string} Safe entity name.
|
||||
* @private
|
||||
*/
|
||||
Names.prototype.safeName_ = function(name) {
|
||||
if (!name) {
|
||||
name = Msg['UNNAMED_KEY'] || 'unnamed';
|
||||
} else {
|
||||
// Unfortunately names in non-latin characters will look like
|
||||
// _E9_9F_B3_E4_B9_90 which is pretty meaningless.
|
||||
// https://github.com/google/blockly/issues/1654
|
||||
name = encodeURI(name.replace(/ /g, '_')).replace(/[^\w]/g, '_');
|
||||
// Most languages don't allow names with leading numbers.
|
||||
if ('0123456789'.indexOf(name[0]) !== -1) {
|
||||
name = 'my_' + name;
|
||||
}
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Do the given two entity names refer to the same entity?
|
||||
* Blockly names are case-insensitive.
|
||||
* @param {string} name1 First name.
|
||||
* @param {string} name2 Second name.
|
||||
* @return {boolean} True if names are the same.
|
||||
*/
|
||||
Names.equals = function(name1, name2) {
|
||||
// name1.localeCompare(name2) is slower.
|
||||
return name1.toLowerCase() === name2.toLowerCase();
|
||||
};
|
||||
|
||||
exports.Names = Names;
|
||||
|
||||
@@ -27,297 +27,302 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
|
||||
|
||||
/**
|
||||
* Class for a pair of scrollbars. Horizontal and vertical.
|
||||
* @param {!WorkspaceSvg} workspace Workspace to bind the scrollbars to.
|
||||
* @param {boolean=} addHorizontal Whether to add a horizontal scrollbar.
|
||||
* Defaults to true.
|
||||
* @param {boolean=} addVertical Whether to add a vertical scrollbar. Defaults
|
||||
* to true.
|
||||
* @param {string=} opt_class A class to be applied to these scrollbars.
|
||||
* @param {number=} opt_margin The margin to apply to these scrollbars.
|
||||
* @constructor
|
||||
* @alias Blockly.ScrollbarPair
|
||||
*/
|
||||
const ScrollbarPair = function(
|
||||
workspace, addHorizontal, addVertical, opt_class, opt_margin) {
|
||||
const ScrollbarPair = class {
|
||||
/**
|
||||
* The workspace this scrollbar pair is bound to.
|
||||
* @type {!WorkspaceSvg}
|
||||
* @private
|
||||
* @param {!WorkspaceSvg} workspace Workspace to bind the scrollbars to.
|
||||
* @param {boolean=} addHorizontal Whether to add a horizontal scrollbar.
|
||||
* Defaults to true.
|
||||
* @param {boolean=} addVertical Whether to add a vertical scrollbar. Defaults
|
||||
* to true.
|
||||
* @param {string=} opt_class A class to be applied to these scrollbars.
|
||||
* @param {number=} opt_margin The margin to apply to these scrollbars.
|
||||
* @alias Blockly.ScrollbarPair
|
||||
*/
|
||||
this.workspace_ = workspace;
|
||||
constructor(workspace, addHorizontal, addVertical, opt_class, opt_margin) {
|
||||
/**
|
||||
* The workspace this scrollbar pair is bound to.
|
||||
* @type {!WorkspaceSvg}
|
||||
* @private
|
||||
*/
|
||||
this.workspace_ = workspace;
|
||||
|
||||
addHorizontal = addHorizontal === undefined ? true : addHorizontal;
|
||||
addVertical = addVertical === undefined ? true : addVertical;
|
||||
const isPair = addHorizontal && addVertical;
|
||||
addHorizontal = addHorizontal === undefined ? true : addHorizontal;
|
||||
addVertical = addVertical === undefined ? true : addVertical;
|
||||
const isPair = addHorizontal && addVertical;
|
||||
|
||||
if (addHorizontal) {
|
||||
this.hScroll =
|
||||
new Scrollbar(workspace, true, isPair, opt_class, opt_margin);
|
||||
}
|
||||
if (addVertical) {
|
||||
this.vScroll =
|
||||
new Scrollbar(workspace, false, isPair, opt_class, opt_margin);
|
||||
}
|
||||
if (addHorizontal) {
|
||||
this.hScroll =
|
||||
new Scrollbar(workspace, true, isPair, opt_class, opt_margin);
|
||||
}
|
||||
if (addVertical) {
|
||||
this.vScroll =
|
||||
new Scrollbar(workspace, false, isPair, opt_class, opt_margin);
|
||||
}
|
||||
|
||||
if (isPair) {
|
||||
this.corner_ = dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
'height': Scrollbar.scrollbarThickness,
|
||||
'width': Scrollbar.scrollbarThickness,
|
||||
'class': 'blocklyScrollbarBackground',
|
||||
},
|
||||
null);
|
||||
dom.insertAfter(this.corner_, workspace.getBubbleCanvas());
|
||||
if (isPair) {
|
||||
this.corner_ = dom.createSvgElement(
|
||||
Svg.RECT, {
|
||||
'height': Scrollbar.scrollbarThickness,
|
||||
'width': Scrollbar.scrollbarThickness,
|
||||
'class': 'blocklyScrollbarBackground',
|
||||
},
|
||||
null);
|
||||
dom.insertAfter(this.corner_, workspace.getBubbleCanvas());
|
||||
}
|
||||
|
||||
/**
|
||||
* Previously recorded metrics from the workspace.
|
||||
* @type {?Metrics}
|
||||
* @private
|
||||
*/
|
||||
this.oldHostMetrics_ = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Previously recorded metrics from the workspace.
|
||||
* @type {?Metrics}
|
||||
* @private
|
||||
* Dispose of this pair of scrollbars.
|
||||
* Unlink from all DOM elements to prevent memory leaks.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
this.oldHostMetrics_ = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispose of this pair of scrollbars.
|
||||
* Unlink from all DOM elements to prevent memory leaks.
|
||||
* @suppress {checkTypes}
|
||||
*/
|
||||
ScrollbarPair.prototype.dispose = function() {
|
||||
dom.removeNode(this.corner_);
|
||||
this.corner_ = null;
|
||||
this.workspace_ = null;
|
||||
this.oldHostMetrics_ = null;
|
||||
if (this.hScroll) {
|
||||
this.hScroll.dispose();
|
||||
this.hScroll = null;
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.dispose();
|
||||
this.vScroll = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Recalculate both of the scrollbars' locations and lengths.
|
||||
* Also reposition the corner rectangle.
|
||||
*/
|
||||
ScrollbarPair.prototype.resize = function() {
|
||||
// Look up the host metrics once, and use for both scrollbars.
|
||||
const hostMetrics = this.workspace_.getMetrics();
|
||||
if (!hostMetrics) {
|
||||
// Host element is likely not visible.
|
||||
return;
|
||||
}
|
||||
|
||||
// Only change the scrollbars if there has been a change in metrics.
|
||||
let resizeH = false;
|
||||
let resizeV = false;
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.viewWidth !== hostMetrics.viewWidth ||
|
||||
this.oldHostMetrics_.viewHeight !== hostMetrics.viewHeight ||
|
||||
this.oldHostMetrics_.absoluteTop !== hostMetrics.absoluteTop ||
|
||||
this.oldHostMetrics_.absoluteLeft !== hostMetrics.absoluteLeft) {
|
||||
// The window has been resized or repositioned.
|
||||
resizeH = true;
|
||||
resizeV = true;
|
||||
} else {
|
||||
// Has the content been resized or moved?
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.scrollWidth !== hostMetrics.scrollWidth ||
|
||||
this.oldHostMetrics_.viewLeft !== hostMetrics.viewLeft ||
|
||||
this.oldHostMetrics_.scrollLeft !== hostMetrics.scrollLeft) {
|
||||
resizeH = true;
|
||||
}
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.scrollHeight !== hostMetrics.scrollHeight ||
|
||||
this.oldHostMetrics_.viewTop !== hostMetrics.viewTop ||
|
||||
this.oldHostMetrics_.scrollTop !== hostMetrics.scrollTop) {
|
||||
resizeV = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (resizeH || resizeV) {
|
||||
try {
|
||||
eventUtils.disable();
|
||||
if (this.hScroll && resizeH) {
|
||||
this.hScroll.resize(hostMetrics);
|
||||
}
|
||||
if (this.vScroll && resizeV) {
|
||||
this.vScroll.resize(hostMetrics);
|
||||
}
|
||||
} finally {
|
||||
eventUtils.enable();
|
||||
}
|
||||
this.workspace_.maybeFireViewportChangeEvent();
|
||||
}
|
||||
|
||||
if (this.hScroll && this.vScroll) {
|
||||
// Reposition the corner square.
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.viewWidth !== hostMetrics.viewWidth ||
|
||||
this.oldHostMetrics_.absoluteLeft !== hostMetrics.absoluteLeft) {
|
||||
this.corner_.setAttribute('x', this.vScroll.position.x);
|
||||
}
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.viewHeight !== hostMetrics.viewHeight ||
|
||||
this.oldHostMetrics_.absoluteTop !== hostMetrics.absoluteTop) {
|
||||
this.corner_.setAttribute('y', this.hScroll.position.y);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the current metrics to potentially short-cut the next resize event.
|
||||
this.oldHostMetrics_ = hostMetrics;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether scrolling horizontally is enabled.
|
||||
* @return {boolean} True if horizontal scroll is enabled.
|
||||
*/
|
||||
ScrollbarPair.prototype.canScrollHorizontally = function() {
|
||||
return !!this.hScroll;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether scrolling vertically is enabled.
|
||||
* @return {boolean} True if vertical scroll is enabled.
|
||||
*/
|
||||
ScrollbarPair.prototype.canScrollVertically = function() {
|
||||
return !!this.vScroll;
|
||||
};
|
||||
|
||||
/**
|
||||
* Record the origin of the workspace that the scrollbar is in, in pixels
|
||||
* relative to the injection div origin. This is for times when the scrollbar is
|
||||
* used in an object whose origin isn't the same as the main workspace
|
||||
* (e.g. in a flyout.)
|
||||
* @param {number} x The x coordinate of the scrollbar's origin, in CSS pixels.
|
||||
* @param {number} y The y coordinate of the scrollbar's origin, in CSS pixels.
|
||||
* @package
|
||||
*/
|
||||
ScrollbarPair.prototype.setOrigin = function(x, y) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.setOrigin(x, y);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.setOrigin(x, y);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the handles of both scrollbars.
|
||||
* @param {number} x The horizontal content displacement, relative to the view
|
||||
* in pixels.
|
||||
* @param {number} y The vertical content displacement, relative to the view in
|
||||
* pixels.
|
||||
* @param {boolean} updateMetrics Whether to update metrics on this set call.
|
||||
* Defaults to true.
|
||||
*/
|
||||
ScrollbarPair.prototype.set = function(x, y, updateMetrics) {
|
||||
// This function is equivalent to:
|
||||
// this.hScroll.set(x);
|
||||
// this.vScroll.set(y);
|
||||
// However, that calls setMetrics twice which causes a chain of
|
||||
// getAttribute->setAttribute->getAttribute resulting in an extra layout pass.
|
||||
// Combining them speeds up rendering.
|
||||
if (this.hScroll) {
|
||||
this.hScroll.set(x, false);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.set(y, false);
|
||||
}
|
||||
|
||||
if (updateMetrics || updateMetrics === undefined) {
|
||||
// Update metrics.
|
||||
const xyRatio = {};
|
||||
dispose() {
|
||||
dom.removeNode(this.corner_);
|
||||
this.corner_ = null;
|
||||
this.workspace_ = null;
|
||||
this.oldHostMetrics_ = null;
|
||||
if (this.hScroll) {
|
||||
xyRatio.x = this.hScroll.getRatio_();
|
||||
this.hScroll.dispose();
|
||||
this.hScroll = null;
|
||||
}
|
||||
if (this.vScroll) {
|
||||
xyRatio.y = this.vScroll.getRatio_();
|
||||
this.vScroll.dispose();
|
||||
this.vScroll = null;
|
||||
}
|
||||
this.workspace_.setMetrics(xyRatio);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the handle of the horizontal scrollbar to be at a certain position in
|
||||
* CSS pixels relative to its parents.
|
||||
* @param {number} x Horizontal scroll value.
|
||||
*/
|
||||
ScrollbarPair.prototype.setX = function(x) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.set(x, true);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Recalculate both of the scrollbars' locations and lengths.
|
||||
* Also reposition the corner rectangle.
|
||||
*/
|
||||
resize() {
|
||||
// Look up the host metrics once, and use for both scrollbars.
|
||||
const hostMetrics = this.workspace_.getMetrics();
|
||||
if (!hostMetrics) {
|
||||
// Host element is likely not visible.
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the handle of the vertical scrollbar to be at a certain position in
|
||||
* CSS pixels relative to its parents.
|
||||
* @param {number} y Vertical scroll value.
|
||||
*/
|
||||
ScrollbarPair.prototype.setY = function(y) {
|
||||
if (this.vScroll) {
|
||||
this.vScroll.set(y, true);
|
||||
}
|
||||
};
|
||||
// Only change the scrollbars if there has been a change in metrics.
|
||||
let resizeH = false;
|
||||
let resizeV = false;
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.viewWidth !== hostMetrics.viewWidth ||
|
||||
this.oldHostMetrics_.viewHeight !== hostMetrics.viewHeight ||
|
||||
this.oldHostMetrics_.absoluteTop !== hostMetrics.absoluteTop ||
|
||||
this.oldHostMetrics_.absoluteLeft !== hostMetrics.absoluteLeft) {
|
||||
// The window has been resized or repositioned.
|
||||
resizeH = true;
|
||||
resizeV = true;
|
||||
} else {
|
||||
// Has the content been resized or moved?
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.scrollWidth !== hostMetrics.scrollWidth ||
|
||||
this.oldHostMetrics_.viewLeft !== hostMetrics.viewLeft ||
|
||||
this.oldHostMetrics_.scrollLeft !== hostMetrics.scrollLeft) {
|
||||
resizeH = true;
|
||||
}
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.scrollHeight !== hostMetrics.scrollHeight ||
|
||||
this.oldHostMetrics_.viewTop !== hostMetrics.viewTop ||
|
||||
this.oldHostMetrics_.scrollTop !== hostMetrics.scrollTop) {
|
||||
resizeV = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this scrollbar's container is visible.
|
||||
* @param {boolean} visible Whether the container is visible.
|
||||
*/
|
||||
ScrollbarPair.prototype.setContainerVisible = function(visible) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.setContainerVisible(visible);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.setContainerVisible(visible);
|
||||
}
|
||||
};
|
||||
if (resizeH || resizeV) {
|
||||
try {
|
||||
eventUtils.disable();
|
||||
if (this.hScroll && resizeH) {
|
||||
this.hScroll.resize(hostMetrics);
|
||||
}
|
||||
if (this.vScroll && resizeV) {
|
||||
this.vScroll.resize(hostMetrics);
|
||||
}
|
||||
} finally {
|
||||
eventUtils.enable();
|
||||
}
|
||||
this.workspace_.maybeFireViewportChangeEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* If any of the scrollbars are visible. Non-paired scrollbars may disappear
|
||||
* when they aren't needed.
|
||||
* @return {boolean} True if visible.
|
||||
*/
|
||||
ScrollbarPair.prototype.isVisible = function() {
|
||||
let isVisible = false;
|
||||
if (this.hScroll) {
|
||||
isVisible = this.hScroll.isVisible();
|
||||
}
|
||||
if (this.vScroll) {
|
||||
isVisible = isVisible || this.vScroll.isVisible();
|
||||
}
|
||||
return isVisible;
|
||||
};
|
||||
if (this.hScroll && this.vScroll) {
|
||||
// Reposition the corner square.
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.viewWidth !== hostMetrics.viewWidth ||
|
||||
this.oldHostMetrics_.absoluteLeft !== hostMetrics.absoluteLeft) {
|
||||
this.corner_.setAttribute('x', this.vScroll.position.x);
|
||||
}
|
||||
if (!this.oldHostMetrics_ ||
|
||||
this.oldHostMetrics_.viewHeight !== hostMetrics.viewHeight ||
|
||||
this.oldHostMetrics_.absoluteTop !== hostMetrics.absoluteTop) {
|
||||
this.corner_.setAttribute('y', this.hScroll.position.y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the scrollbars' locations within their path and length.
|
||||
* This should be called when the contents of the workspace have changed.
|
||||
* @param {!Metrics} hostMetrics A data structure describing all
|
||||
* the required dimensions, possibly fetched from the host object.
|
||||
*/
|
||||
ScrollbarPair.prototype.resizeContent = function(hostMetrics) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.resizeContentHorizontal(hostMetrics);
|
||||
// Cache the current metrics to potentially short-cut the next resize event.
|
||||
this.oldHostMetrics_ = hostMetrics;
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.resizeContentVertical(hostMetrics);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Recalculates the scrollbars' locations on the screen and path length.
|
||||
* This should be called when the layout or size of the window has changed.
|
||||
* @param {!Metrics} hostMetrics A data structure describing all
|
||||
* the required dimensions, possibly fetched from the host object.
|
||||
*/
|
||||
ScrollbarPair.prototype.resizeView = function(hostMetrics) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.resizeViewHorizontal(hostMetrics);
|
||||
/**
|
||||
* Returns whether scrolling horizontally is enabled.
|
||||
* @return {boolean} True if horizontal scroll is enabled.
|
||||
*/
|
||||
canScrollHorizontally() {
|
||||
return !!this.hScroll;
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.resizeViewVertical(hostMetrics);
|
||||
|
||||
/**
|
||||
* Returns whether scrolling vertically is enabled.
|
||||
* @return {boolean} True if vertical scroll is enabled.
|
||||
*/
|
||||
canScrollVertically() {
|
||||
return !!this.vScroll;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record the origin of the workspace that the scrollbar is in, in pixels
|
||||
* relative to the injection div origin. This is for times when the scrollbar
|
||||
* is used in an object whose origin isn't the same as the main workspace
|
||||
* (e.g. in a flyout.)
|
||||
* @param {number} x The x coordinate of the scrollbar's origin, in CSS
|
||||
* pixels.
|
||||
* @param {number} y The y coordinate of the scrollbar's origin, in CSS
|
||||
* pixels.
|
||||
* @package
|
||||
*/
|
||||
setOrigin(x, y) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.setOrigin(x, y);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.setOrigin(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the handles of both scrollbars.
|
||||
* @param {number} x The horizontal content displacement, relative to the view
|
||||
* in pixels.
|
||||
* @param {number} y The vertical content displacement, relative to the view
|
||||
* in
|
||||
* pixels.
|
||||
* @param {boolean} updateMetrics Whether to update metrics on this set call.
|
||||
* Defaults to true.
|
||||
*/
|
||||
set(x, y, updateMetrics) {
|
||||
// This function is equivalent to:
|
||||
// this.hScroll.set(x);
|
||||
// this.vScroll.set(y);
|
||||
// However, that calls setMetrics twice which causes a chain of
|
||||
// getAttribute->setAttribute->getAttribute resulting in an extra layout
|
||||
// pass. Combining them speeds up rendering.
|
||||
if (this.hScroll) {
|
||||
this.hScroll.set(x, false);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.set(y, false);
|
||||
}
|
||||
|
||||
if (updateMetrics || updateMetrics === undefined) {
|
||||
// Update metrics.
|
||||
const xyRatio = {};
|
||||
if (this.hScroll) {
|
||||
xyRatio.x = this.hScroll.getRatio_();
|
||||
}
|
||||
if (this.vScroll) {
|
||||
xyRatio.y = this.vScroll.getRatio_();
|
||||
}
|
||||
this.workspace_.setMetrics(xyRatio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the handle of the horizontal scrollbar to be at a certain position in
|
||||
* CSS pixels relative to its parents.
|
||||
* @param {number} x Horizontal scroll value.
|
||||
*/
|
||||
setX(x) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.set(x, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the handle of the vertical scrollbar to be at a certain position in
|
||||
* CSS pixels relative to its parents.
|
||||
* @param {number} y Vertical scroll value.
|
||||
*/
|
||||
setY(y) {
|
||||
if (this.vScroll) {
|
||||
this.vScroll.set(y, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this scrollbar's container is visible.
|
||||
* @param {boolean} visible Whether the container is visible.
|
||||
*/
|
||||
setContainerVisible(visible) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.setContainerVisible(visible);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.setContainerVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If any of the scrollbars are visible. Non-paired scrollbars may disappear
|
||||
* when they aren't needed.
|
||||
* @return {boolean} True if visible.
|
||||
*/
|
||||
isVisible() {
|
||||
let isVisible = false;
|
||||
if (this.hScroll) {
|
||||
isVisible = this.hScroll.isVisible();
|
||||
}
|
||||
if (this.vScroll) {
|
||||
isVisible = isVisible || this.vScroll.isVisible();
|
||||
}
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the scrollbars' locations within their path and length.
|
||||
* This should be called when the contents of the workspace have changed.
|
||||
* @param {!Metrics} hostMetrics A data structure describing all
|
||||
* the required dimensions, possibly fetched from the host object.
|
||||
*/
|
||||
resizeContent(hostMetrics) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.resizeContentHorizontal(hostMetrics);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.resizeContentVertical(hostMetrics);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the scrollbars' locations on the screen and path length.
|
||||
* This should be called when the layout or size of the window has changed.
|
||||
* @param {!Metrics} hostMetrics A data structure describing all
|
||||
* the required dimensions, possibly fetched from the host object.
|
||||
*/
|
||||
resizeView(hostMetrics) {
|
||||
if (this.hScroll) {
|
||||
this.hScroll.resizeViewHorizontal(hostMetrics);
|
||||
}
|
||||
if (this.vScroll) {
|
||||
this.vScroll.resizeViewVertical(hostMetrics);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user