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:
Rachel Fenichel
2022-01-10 09:18:11 -08:00
committed by GitHub
parent 6ee63894b3
commit 1c74679d83
4 changed files with 1132 additions and 1083 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
};