From 8530e6d537e88c1f7b6b66d588edf5f23256e59e Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 28 Sep 2022 15:51:17 -0700 Subject: [PATCH] fix: adding and removing css classes that contained spaces (#6455) * fix: adding CSS classes * fix: removing css classes * fix: add a test for multiple icon classes * chore: format --- core/block_svg.ts | 4 +-- core/contextmenu.ts | 3 +- core/dropdowndiv.ts | 13 ++++---- core/field.ts | 8 ++--- core/field_checkbox.ts | 3 +- core/field_colour.ts | 7 +++-- core/field_dropdown.ts | 6 ++-- core/field_label.ts | 7 +++-- core/field_multilineinput.ts | 8 ++--- core/field_textinput.ts | 9 +++--- core/icon.ts | 2 +- core/inject.ts | 4 +-- core/menu.ts | 5 +-- core/menuitem.ts | 9 +++--- core/mutator.ts | 4 +-- core/renderers/common/path_object.ts | 4 +-- core/theme_manager.ts | 5 +-- core/toolbox/category.ts | 18 +++++------ core/toolbox/collapsible_category.ts | 5 +-- core/toolbox/separator.ts | 2 +- core/toolbox/toolbox.ts | 14 ++++----- core/utils/dom.ts | 21 +++++++------ core/widgetdiv.ts | 9 +++--- core/workspace_comment_svg.ts | 47 ++++++++++++++++++---------- core/workspace_svg.ts | 8 ++--- tests/mocha/toolbox_test.js | 20 ++++++++++++ 26 files changed, 146 insertions(+), 99 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 128a99fcf..b4bdd5dfa 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -778,10 +778,10 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, (group as AnyDuringMigration).translate_ = ''; (group as AnyDuringMigration).skew_ = ''; common.draggingConnections.push(...this.getConnections_(true)); - this.svgGroup_.classList.add('blocklyDragging'); + dom.addClass(this.svgGroup_, 'blocklyDragging'); } else { common.draggingConnections.length = 0; - this.svgGroup_.classList.remove('blocklyDragging'); + dom.removeClass(this.svgGroup_, 'blocklyDragging'); } // Recurse through all blocks attached under this one. for (let i = 0; i < this.childBlocks_.length; i++) { diff --git a/core/contextmenu.ts b/core/contextmenu.ts index 71e4ce8b4..c7737a597 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -17,6 +17,7 @@ import type {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import * as clipboard from './clipboard.js'; import {config} from './config.js'; +import * as dom from './utils/dom.js'; import type {ContextMenuOption, LegacyContextMenuOption} from './contextmenu_registry.js'; import * as eventUtils from './events/utils.js'; import {Menu} from './menu.js'; @@ -179,7 +180,7 @@ function createWidget_(menu: Menu) { throw Error('Attempting to create a context menu when widget div is null'); } const menuDom = menu.render(div); - menuDom.classList.add('blocklyContextMenu'); + dom.addClass(menuDom, 'blocklyContextMenu'); // Prevent system context menu when right-clicking a Blockly context menu. browserEvents.conditionalBind( (menuDom as EventTarget), 'contextmenu', null, haltPropagation); diff --git a/core/dropdowndiv.ts b/core/dropdowndiv.ts index b263a0be2..27e1d160d 100644 --- a/core/dropdowndiv.ts +++ b/core/dropdowndiv.ts @@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.dropDownDiv'); import type {BlockSvg} from './block_svg.js'; import * as common from './common.js'; +import * as dom from './utils/dom.js'; import type {Field} from './field.js'; import * as math from './utils/math.js'; import {Rect} from './utils/rect.js'; @@ -138,10 +139,10 @@ export function createDom() { // Handle focusin/out events to add a visual indicator when // a child is focused or blurred. div.addEventListener('focusin', function() { - div.classList.add('blocklyFocused'); + dom.addClass(div, 'blocklyFocused'); }); div.addEventListener('focusout', function() { - div.classList.remove('blocklyFocused'); + dom.removeClass(div, 'blocklyFocused'); }); } @@ -311,10 +312,10 @@ export function show( renderedClassName = mainWorkspace.getRenderer().getClassName(); themeClassName = mainWorkspace.getTheme().getClassName(); if (renderedClassName) { - div.classList.add(renderedClassName); + dom.addClass(div, renderedClassName); } if (themeClassName) { - div.classList.add(themeClassName); + dom.addClass(div, themeClassName); } // When we change `translate` multiple times in close succession, @@ -595,11 +596,11 @@ export function hideWithoutAnimation() { owner = null; if (renderedClassName) { - div.classList.remove(renderedClassName); + dom.removeClass(div, renderedClassName); renderedClassName = ''; } if (themeClassName) { - div.classList.remove(themeClassName); + dom.removeClass(div, themeClassName); themeClassName = ''; } (common.getMainWorkspace() as WorkspaceSvg).markFocused(); diff --git a/core/field.ts b/core/field.ts index 32c43a8fe..9f29e6d1f 100644 --- a/core/field.ts +++ b/core/field.ts @@ -492,12 +492,12 @@ export abstract class Field implements IASTNodeLocationSvg, return; } if (this.enabled_ && this.sourceBlock_.isEditable()) { - group.classList.add('blocklyEditableText'); - group.classList.remove('blocklyNonEditableText'); + dom.addClass(group, 'blocklyEditableText'); + dom.removeClass(group, 'blocklyNonEditableText'); group.style.cursor = this.CURSOR; } else { - group.classList.add('blocklyNonEditableText'); - group.classList.remove('blocklyEditableText'); + dom.addClass(group, 'blocklyNonEditableText'); + dom.removeClass(group, 'blocklyEditableText'); group.style.cursor = ''; } } diff --git a/core/field_checkbox.ts b/core/field_checkbox.ts index 9cabb3e58..765705f1a 100644 --- a/core/field_checkbox.ts +++ b/core/field_checkbox.ts @@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.FieldCheckbox'); // Unused import preserved for side-effects. Remove if unneeded. import './events/events_block_change.js'; +import * as dom from './utils/dom.js'; import {FieldConfig, Field} from './field.js'; import * as fieldRegistry from './field_registry.js'; import type {Sentinel} from './utils/sentinel.js'; @@ -111,7 +112,7 @@ export class FieldCheckbox extends Field { override initView() { super.initView(); - this.textElement_.classList.add('blocklyCheckbox'); + dom.addClass(this.textElement_, 'blocklyCheckbox'); this.textElement_.style.display = this.value_ ? 'block' : 'none'; } diff --git a/core/field_colour.ts b/core/field_colour.ts index 4cf4e19d4..f70843053 100644 --- a/core/field_colour.ts +++ b/core/field_colour.ts @@ -18,6 +18,7 @@ import './events/events_block_change.js'; import {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import * as Css from './css.js'; +import * as dom from './utils/dom.js'; import * as dropDownDiv from './dropdowndiv.js'; import {FieldConfig, Field} from './field.js'; import * as fieldRegistry from './field_registry.js'; @@ -438,7 +439,7 @@ export class FieldColour extends Field { (this.picker_ as AnyDuringMigration)!.blur(); const highlighted = this.getHighlighted_(); if (highlighted) { - highlighted.classList.remove('blocklyColourHighlighted'); + dom.removeClass(highlighted, 'blocklyColourHighlighted'); } } @@ -473,10 +474,10 @@ export class FieldColour extends Field { // Unhighlight the current item. const highlighted = this.getHighlighted_(); if (highlighted) { - highlighted.classList.remove('blocklyColourHighlighted'); + dom.removeClass(highlighted, 'blocklyColourHighlighted'); } // Highlight new item. - cell.classList.add('blocklyColourHighlighted'); + dom.addClass(cell, 'blocklyColourHighlighted'); // Set new highlighted index. this.highlightedIndex_ = index; diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index adbca4375..8c274f8a1 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -205,7 +205,7 @@ export class FieldDropdown extends Field { } if (this.borderRect_) { - this.borderRect_.classList.add('blocklyDropdownRect'); + dom.addClass(this.borderRect_, 'blocklyDropdownRect'); } } @@ -276,7 +276,7 @@ export class FieldDropdown extends Field { dropDownDiv.clearContent(); // Element gets created in render. const menuElement = this.menu_!.render(dropDownDiv.getContentDiv()); - menuElement.classList.add('blocklyDropdownMenu'); + dom.addClass(menuElement, 'blocklyDropdownMenu'); if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) { const primaryColour = this.sourceBlock_.isShadow() ? @@ -608,7 +608,7 @@ export class FieldDropdown extends Field { private renderSelectedText_() { // Retrieves the selected option to display through getText_. this.textContent_.nodeValue = this.getDisplayText_(); - this.textElement_.classList.add('blocklyDropdownText'); + dom.addClass(this.textElement_, 'blocklyDropdownText'); this.textElement_.setAttribute('text-anchor', 'start'); // Height and width include the border rect. diff --git a/core/field_label.ts b/core/field_label.ts index ea7baec15..fc77cb144 100644 --- a/core/field_label.ts +++ b/core/field_label.ts @@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.FieldLabel'); +import * as dom from './utils/dom.js'; import {FieldConfig, Field} from './field.js'; import * as fieldRegistry from './field_registry.js'; import * as parsing from './utils/parsing.js'; @@ -75,7 +76,7 @@ export class FieldLabel extends Field { override initView() { this.createTextElement_(); if (this.class_) { - this.textElement_.classList.add(this.class_); + dom.addClass(this.textElement_, this.class_); } } @@ -101,10 +102,10 @@ export class FieldLabel extends Field { setClass(cssClass: string|null) { if (this.textElement_) { if (this.class_) { - this.textElement_.classList.remove(this.class_); + dom.removeClass(this.textElement_, this.class_); } if (cssClass) { - this.textElement_.classList.add(cssClass); + dom.addClass(this.textElement_, cssClass); } } this.class_ = cssClass; diff --git a/core/field_multilineinput.ts b/core/field_multilineinput.ts index 494c8b542..569d04787 100644 --- a/core/field_multilineinput.ts +++ b/core/field_multilineinput.ts @@ -239,9 +239,9 @@ export class FieldMultilineInput extends FieldTextInput { if (this.isBeingEdited_) { const htmlInput = this.htmlInput_ as HTMLElement; if (this.isOverflowedY_) { - htmlInput.classList.add('blocklyHtmlTextAreaInputOverflowedY'); + dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY'); } else { - htmlInput.classList.remove('blocklyHtmlTextAreaInputOverflowedY'); + dom.removeClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY'); } } @@ -258,10 +258,10 @@ export class FieldMultilineInput extends FieldTextInput { } const htmlInput = this.htmlInput_ as HTMLElement; if (!this.isTextValid_) { - htmlInput.classList.add('blocklyInvalidInput'); + dom.addClass(htmlInput, 'blocklyInvalidInput'); aria.setState(htmlInput, aria.State.INVALID, true); } else { - htmlInput.classList.remove('blocklyInvalidInput'); + dom.removeClass(htmlInput, 'blocklyInvalidInput'); aria.setState(htmlInput, aria.State.INVALID, false); } } diff --git a/core/field_textinput.ts b/core/field_textinput.ts index babb4441c..7904f84f4 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -18,6 +18,7 @@ import './events/events_block_change.js'; import type {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; import * as dialog from './dialog.js'; +import * as dom from './utils/dom.js'; import * as dropDownDiv from './dropdowndiv.js'; import * as eventUtils from './events/utils.js'; import {FieldConfig, Field} from './field.js'; @@ -244,10 +245,10 @@ export class FieldTextInput extends Field { this.resizeEditor_(); const htmlInput = this.htmlInput_ as HTMLElement; if (!this.isTextValid_) { - htmlInput.classList.add('blocklyInvalidInput'); + dom.addClass(htmlInput, 'blocklyInvalidInput'); aria.setState(htmlInput, aria.State.INVALID, true); } else { - htmlInput.classList.remove('blocklyInvalidInput'); + dom.removeClass(htmlInput, 'blocklyInvalidInput'); aria.setState(htmlInput, aria.State.INVALID, false); } } @@ -331,7 +332,7 @@ export class FieldTextInput extends Field { eventUtils.setGroup(true); const div = WidgetDiv.getDiv(); - this.getClickTarget_().classList.add('editing'); + dom.addClass(this.getClickTarget_(), 'editing'); const htmlInput = (document.createElement('input')); htmlInput.className = 'blocklyHtmlInput'; @@ -400,7 +401,7 @@ export class FieldTextInput extends Field { style.boxShadow = ''; this.htmlInput_ = null; - this.getClickTarget_().classList.remove('editing'); + dom.removeClass(this.getClickTarget_(), 'editing'); } /** diff --git a/core/icon.ts b/core/icon.ts index 1512f4a25..b771d706f 100644 --- a/core/icon.ts +++ b/core/icon.ts @@ -69,7 +69,7 @@ export abstract class Icon { this.iconGroup_ = dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}); if (this.getBlock().isInFlyout) { - this.iconGroup_.classList.add('blocklyIconGroupReadonly'); + dom.addClass(this.iconGroup_, 'blocklyIconGroupReadonly'); } this.drawIcon_(this.iconGroup_); diff --git a/core/inject.ts b/core/inject.ts index 2ec3a4c26..833075665 100644 --- a/core/inject.ts +++ b/core/inject.ts @@ -172,11 +172,11 @@ function createMainWorkspace( const injectionDiv = mainWorkspace.getInjectionDiv(); const rendererClassName = mainWorkspace.getRenderer().getClassName(); if (rendererClassName) { - injectionDiv.classList.add(rendererClassName); + dom.addClass(injectionDiv, rendererClassName); } const themeClassName = mainWorkspace.getTheme().getClassName(); if (themeClassName) { - injectionDiv.classList.add(themeClassName); + dom.addClass(injectionDiv, themeClassName); } if (!wsOptions.hasCategories && wsOptions.languageTree) { diff --git a/core/menu.ts b/core/menu.ts index 88a43b9fc..482785317 100644 --- a/core/menu.ts +++ b/core/menu.ts @@ -16,6 +16,7 @@ import * as browserEvents from './browser_events.js'; import type {MenuItem} from './menuitem.js'; import * as aria from './utils/aria.js'; import {Coordinate} from './utils/coordinate.js'; +import * as dom from './utils/dom.js'; import {KeyCodes} from './utils/keycodes.js'; import type {Size} from './utils/size.js'; import * as style from './utils/style.js'; @@ -137,7 +138,7 @@ export class Menu { const el = this.getElement(); if (el) { el.focus({preventScroll: true}); - el.classList.add('blocklyFocused'); + dom.addClass(el, 'blocklyFocused'); } } @@ -146,7 +147,7 @@ export class Menu { const el = this.getElement(); if (el) { el.blur(); - el.classList.remove('blocklyFocused'); + dom.removeClass(el, 'blocklyFocused'); } } diff --git a/core/menuitem.ts b/core/menuitem.ts index 721c6cfce..3eec5ac69 100644 --- a/core/menuitem.ts +++ b/core/menuitem.ts @@ -13,6 +13,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.MenuItem'); import * as aria from './utils/aria.js'; +import * as dom from './utils/dom.js'; import * as idGenerator from './utils/idgenerator.js'; @@ -195,11 +196,11 @@ export class MenuItem { const name = 'blocklyMenuItemHighlight'; const nameDep = 'goog-menuitem-highlight'; if (highlight) { - el.classList.add(name); - el.classList.add(nameDep); + dom.addClass(el, name); + dom.addClass(el, nameDep); } else { - el.classList.remove(name); - el.classList.remove(nameDep); + dom.removeClass(el, name); + dom.removeClass(el, nameDep); } } } diff --git a/core/mutator.ts b/core/mutator.ts index 9a3e650c5..41f918a14 100644 --- a/core/mutator.ts +++ b/core/mutator.ts @@ -238,13 +238,13 @@ export class Mutator extends Icon { if (!this.getBlock().isInFlyout) { if (this.getBlock().isEditable()) { if (this.iconGroup_) { - this.iconGroup_.classList.remove('blocklyIconGroupReadonly'); + dom.removeClass(this.iconGroup_, 'blocklyIconGroupReadonly'); } } else { // Close any mutator bubble. Icon is not clickable. this.setVisible(false); if (this.iconGroup_) { - this.iconGroup_.classList.add('blocklyIconGroupReadonly'); + dom.addClass(this.iconGroup_, 'blocklyIconGroupReadonly'); } } } diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index 66951d414..712fce28b 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -170,9 +170,9 @@ export class PathObject implements IPathObject { return; } if (add) { - this.svgRoot.classList.add(className); + dom.addClass(this.svgRoot, className); } else { - this.svgRoot.classList.remove(className); + dom.removeClass(this.svgRoot, className); } } diff --git a/core/theme_manager.ts b/core/theme_manager.ts index 1a7bdd3f0..33fa9f483 100644 --- a/core/theme_manager.ts +++ b/core/theme_manager.ts @@ -15,6 +15,7 @@ goog.declareModuleId('Blockly.ThemeManager'); import type {Theme} from './theme.js'; import * as arrayUtils from './utils/array.js'; +import * as dom from './utils/dom.js'; import type {Workspace} from './workspace.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -62,12 +63,12 @@ export class ThemeManager { if (prevTheme) { const oldClassName = prevTheme.getClassName(); if (oldClassName) { - injectionDiv.classList.remove(oldClassName); + dom.removeClass(injectionDiv, oldClassName); } } const newClassName = this.theme.getClassName(); if (newClassName) { - injectionDiv.classList.add(newClassName); + dom.addClass(injectionDiv, newClassName); } } diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts index dfb40e2c8..9a2c125f6 100644 --- a/core/toolbox/category.ts +++ b/core/toolbox/category.ts @@ -219,7 +219,7 @@ export class ToolboxCategory extends ToolboxItem implements const container = document.createElement('div'); const className = this.cssConfig_['container']; if (className) { - container.classList.add(className); + dom.addClass(container, className); } return container; } @@ -234,7 +234,7 @@ export class ToolboxCategory extends ToolboxItem implements const rowDiv = document.createElement('div'); const className = this.cssConfig_['row']; if (className) { - rowDiv.classList.add(className); + dom.addClass(rowDiv, className); } const nestedPadding = `${ToolboxCategory.nestedPadding * this.getLevel()}px`; @@ -253,7 +253,7 @@ export class ToolboxCategory extends ToolboxItem implements const contentsContainer = document.createElement('div'); const className = this.cssConfig_['rowcontentcontainer']; if (className) { - contentsContainer.classList.add(className); + dom.addClass(contentsContainer, className); } return contentsContainer; } @@ -268,7 +268,7 @@ export class ToolboxCategory extends ToolboxItem implements if (!this.parentToolbox_.isHorizontal()) { const className = this.cssConfig_['icon']; if (className) { - toolboxIcon.classList.add(className); + dom.addClass(toolboxIcon, className); } } @@ -289,7 +289,7 @@ export class ToolboxCategory extends ToolboxItem implements toolboxLabel.textContent = name; const className = this.cssConfig_['label']; if (className) { - toolboxLabel.classList.add(className); + dom.addClass(toolboxLabel, className); } return toolboxLabel; } @@ -420,7 +420,7 @@ export class ToolboxCategory extends ToolboxItem implements } const className = this.cssConfig_['openicon']; if (className) { - iconDiv.classList.add(className); + dom.addClass(iconDiv, className); } } @@ -439,7 +439,7 @@ export class ToolboxCategory extends ToolboxItem implements } const className = this.cssConfig_['closedicon']; if (className) { - iconDiv.classList.add(className); + dom.addClass(iconDiv, className); } } @@ -527,12 +527,12 @@ export class ToolboxCategory extends ToolboxItem implements this.parseColour_(ToolboxCategory.defaultBackgroundColour); this.rowDiv_.style.backgroundColor = this.colour_ || defaultColour; if (className) { - this.rowDiv_.classList.add(className); + dom.addClass(this.rowDiv_, className); } } else { this.rowDiv_.style.backgroundColor = ''; if (className) { - this.rowDiv_.classList.remove(className); + dom.removeClass(this.rowDiv_, className); } } aria.setState(this.htmlDiv_ as Element, aria.State.SELECTED, isSelected); diff --git a/core/toolbox/collapsible_category.ts b/core/toolbox/collapsible_category.ts index 8f7f036bc..1c6ff27fd 100644 --- a/core/toolbox/collapsible_category.ts +++ b/core/toolbox/collapsible_category.ts @@ -17,6 +17,7 @@ import type {IToolbox} from '../interfaces/i_toolbox.js'; import type {IToolboxItem} from '../interfaces/i_toolbox_item.js'; import * as registry from '../registry.js'; import * as aria from '../utils/aria.js'; +import * as dom from '../utils/dom.js'; import * as toolbox from '../utils/toolbox.js'; import {ToolboxCategory} from './category.js'; @@ -142,7 +143,7 @@ export class CollapsibleToolboxCategory extends ToolboxCategory implements if (!this.parentToolbox_.isHorizontal()) { const className = (this.cssConfig_ as AnyDuringMigration)['icon']; if (className) { - toolboxIcon.classList.add(className); + dom.addClass(toolboxIcon, className); } toolboxIcon.style.visibility = 'visible'; } @@ -162,7 +163,7 @@ export class CollapsibleToolboxCategory extends ToolboxCategory implements const contentsContainer = document.createElement('div'); const className = (this.cssConfig_ as AnyDuringMigration)['contents']; if (className) { - contentsContainer.classList.add(className); + dom.addClass(contentsContainer, className); } for (let i = 0; i < subcategories.length; i++) { diff --git a/core/toolbox/separator.ts b/core/toolbox/separator.ts index 5ac71a84e..26fac7e32 100644 --- a/core/toolbox/separator.ts +++ b/core/toolbox/separator.ts @@ -61,7 +61,7 @@ export class ToolboxSeparator extends ToolboxItem { const container = document.createElement('div'); const className = this.cssConfig_['container']; if (className) { - container.classList.add(className); + dom.addClass(container, className); } this.htmlDiv_ = container; return container; diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts index 80d49517b..3c9e3c23b 100644 --- a/core/toolbox/toolbox.ts +++ b/core/toolbox/toolbox.ts @@ -198,8 +198,8 @@ export class Toolbox extends DeleteArea implements IAutoHideable, protected createContainer_(): HTMLDivElement { const toolboxContainer = (document.createElement('div')); toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v'); - toolboxContainer.classList.add('blocklyToolboxDiv'); - toolboxContainer.classList.add('blocklyNonSelectable'); + dom.addClass(toolboxContainer, 'blocklyToolboxDiv'); + dom.addClass(toolboxContainer, 'blocklyNonSelectable'); toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR'); return toolboxContainer; } @@ -211,7 +211,7 @@ export class Toolbox extends DeleteArea implements IAutoHideable, */ protected createContentsContainer_(): HTMLDivElement { const contentsContainer = (document.createElement('div')); - contentsContainer.classList.add('blocklyToolboxContents'); + dom.addClass(contentsContainer, 'blocklyToolboxContents'); if (this.isHorizontal()) { contentsContainer.style.flexDirection = 'row'; } @@ -453,8 +453,8 @@ export class Toolbox extends DeleteArea implements IAutoHideable, * @internal */ addStyle(style: string) { - if (style) { - this.HtmlDiv?.classList.add(style); + if (style && this.HtmlDiv) { + dom.addClass(this.HtmlDiv, style); } } @@ -465,8 +465,8 @@ export class Toolbox extends DeleteArea implements IAutoHideable, * @internal */ removeStyle(style: string) { - if (style) { - this.HtmlDiv?.classList.remove(style); + if (style && this.HtmlDiv) { + dom.removeClass(this.HtmlDiv, style); } } diff --git a/core/utils/dom.ts b/core/utils/dom.ts index 8a49a3f05..c8e799fe2 100644 --- a/core/utils/dom.ts +++ b/core/utils/dom.ts @@ -85,46 +85,49 @@ export function createSvgElement( /** * Add a CSS class to a element. * + * Handles multiple space-separated classes for legacy reasons. + * * @param element DOM element to add class to. * @param className Name of class to add. * @returns True if class was added, false if already present. * @alias Blockly.utils.dom.addClass */ export function addClass(element: Element, className: string): boolean { - if (element.classList.contains(className)) { + const classNames = className.split(' '); + if (classNames.every((name) => element.classList.contains(name))) { return false; } - element.classList.add(className); + element.classList.add(...classNames); return true; } /** - * Removes multiple calsses from an element. + * Removes multiple classes from an element. * * @param element DOM element to remove classes from. * @param classNames A string of one or multiple class names for an element. * @alias Blockly.utils.dom.removeClasses */ export function removeClasses(element: Element, classNames: string) { - const classList = classNames.split(' '); - for (let i = 0; i < classList.length; i++) { - element.classList.remove(classList[i]); - } + element.classList.remove(...classNames.split(' ')); } /** * Remove a CSS class from a element. * + * Handles multiple space-separated classes for legacy reasons. + * * @param element DOM element to remove class from. * @param className Name of class to remove. * @returns True if class was removed, false if never present. * @alias Blockly.utils.dom.removeClass */ export function removeClass(element: Element, className: string): boolean { - if (!element.classList.contains(className)) { + const classNames = className.split(' '); + if (classNames.every((name) => !element.classList.contains(name))) { return false; } - element.classList.remove(className); + element.classList.remove(...classNames); return true; } diff --git a/core/widgetdiv.ts b/core/widgetdiv.ts index 2b21487ad..1ca04092f 100644 --- a/core/widgetdiv.ts +++ b/core/widgetdiv.ts @@ -15,6 +15,7 @@ import * as goog from '../closure/goog/goog.js'; goog.declareModuleId('Blockly.WidgetDiv'); import * as common from './common.js'; +import * as dom from './utils/dom.js'; import type {Rect} from './utils/rect.js'; import type {Size} from './utils/size.js'; import type {WorkspaceSvg} from './workspace_svg.js'; @@ -93,10 +94,10 @@ export function show(newOwner: unknown, rtl: boolean, newDispose: () => void) { rendererClassName = mainWorkspace.getRenderer().getClassName(); themeClassName = mainWorkspace.getTheme().getClassName(); if (rendererClassName) { - div.classList.add(rendererClassName); + dom.addClass(div, rendererClassName); } if (themeClassName) { - div.classList.add(themeClassName); + dom.addClass(div, themeClassName); } } @@ -121,11 +122,11 @@ export function hide() { div.textContent = ''; if (rendererClassName) { - div.classList.remove(rendererClassName); + dom.removeClass(div, rendererClassName); rendererClassName = ''; } if (themeClassName) { - div.classList.remove(themeClassName); + dom.removeClass(div, themeClassName); themeClassName = ''; } (common.getMainWorkspace() as WorkspaceSvg).markFocused(); diff --git a/core/workspace_comment_svg.ts b/core/workspace_comment_svg.ts index 2c1d4e025..1af608b41 100644 --- a/core/workspace_comment_svg.ts +++ b/core/workspace_comment_svg.ts @@ -263,7 +263,7 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements * @internal */ addSelect() { - this.svgGroup_.classList.add('blocklySelected'); + dom.addClass(this.svgGroup_, 'blocklySelected'); this.setFocus(); } @@ -273,7 +273,7 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements * @internal */ removeSelect() { - this.svgGroup_.classList.add('blocklySelected'); + dom.addClass(this.svgGroup_, 'blocklySelected'); this.blurFocus(); } @@ -283,7 +283,7 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements * @internal */ addFocus() { - this.svgGroup_.classList.add('blocklyFocused'); + dom.addClass(this.svgGroup_, 'blocklyFocused'); } /** @@ -292,7 +292,7 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements * @internal */ removeFocus() { - this.svgGroup_.classList.remove('blocklyFocused'); + dom.removeClass(this.svgGroup_, 'blocklyFocused'); } /** @@ -473,9 +473,9 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements */ updateMovable() { if (this.isMovable()) { - this.svgGroup_.classList.add('blocklyDraggable'); + dom.addClass(this.svgGroup_, 'blocklyDraggable'); } else { - this.svgGroup_.classList.remove('blocklyDraggable'); + dom.removeClass(this.svgGroup_, 'blocklyDraggable'); } } @@ -514,9 +514,9 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements const group = this.getSvgRoot(); (group as AnyDuringMigration).translate_ = ''; (group as AnyDuringMigration).skew_ = ''; - this.svgGroup_.classList.add('blocklyDragging'); + dom.addClass(this.svgGroup_, 'blocklyDragging'); } else { - this.svgGroup_.classList.remove('blocklyDragging'); + dom.removeClass(this.svgGroup_, 'blocklyDragging'); } } @@ -561,9 +561,9 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements */ setDeleteStyle(enable: boolean) { if (enable) { - this.svgGroup_.classList.add('blocklyDraggingDelete'); + dom.addClass(this.svgGroup_, 'blocklyDraggingDelete'); } else { - this.svgGroup_.classList.remove('blocklyDraggingDelete'); + dom.removeClass(this.svgGroup_, 'blocklyDraggingDelete'); } } @@ -853,7 +853,9 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements */ private deleteMouseDown_(e: Event) { // Highlight the delete icon. - this.deleteIconBorder_?.classList.add('blocklyDeleteIconHighlighted'); + if (this.deleteIconBorder_) { + dom.addClass(this.deleteIconBorder_, 'blocklyDeleteIconHighlighted'); + } // This event has been handled. No need to bubble up to the document. e.stopPropagation(); } @@ -865,7 +867,9 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements */ private deleteMouseOut_(_e: Event) { // Restore highlight on the delete icon. - this.deleteIconBorder_?.classList.remove('blocklyDeleteIconHighlighted'); + if (this.deleteIconBorder_) { + dom.removeClass(this.deleteIconBorder_, 'blocklyDeleteIconHighlighted'); + } } /** @@ -1016,8 +1020,13 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements } this.textarea_!.focus(); this.addFocus(); - this.svgRectTarget_?.classList.add('blocklyCommentTargetFocused'); - this.svgHandleTarget_?.classList.add('blocklyCommentHandleTargetFocused'); + if (this.svgRectTarget_) { + dom.addClass(this.svgRectTarget_, 'blocklyCommentTargetFocused'); + } + if (this.svgHandleTarget_) { + dom.addClass( + this.svgHandleTarget_, 'blocklyCommentHandleTargetFocused'); + } }, 0); } @@ -1036,9 +1045,13 @@ export class WorkspaceCommentSvg extends WorkspaceComment implements this.textarea_!.blur(); this.removeFocus(); - this.svgRectTarget_?.classList.remove('blocklyCommentTargetFocused'); - this.svgHandleTarget_?.classList.remove( - 'blocklyCommentHandleTargetFocused'); + if (this.svgRectTarget_) { + dom.removeClass(this.svgRectTarget_, 'blocklyCommentTargetFocused'); + } + if (this.svgHandleTarget_) { + dom.removeClass( + this.svgHandleTarget_, 'blocklyCommentHandleTargetFocused'); + } }, 0); } diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 415b7a23e..59c0feacb 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -2080,8 +2080,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ beginCanvasTransition() { - this.svgBlockCanvas_.classList.add('blocklyCanvasTransitioning'); - this.svgBubbleCanvas_.classList.add('blocklyCanvasTransitioning'); + dom.addClass(this.svgBlockCanvas_, 'blocklyCanvasTransitioning'); + dom.addClass(this.svgBubbleCanvas_, 'blocklyCanvasTransitioning'); } /** @@ -2090,8 +2090,8 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @internal */ endCanvasTransition() { - this.svgBlockCanvas_.classList.remove('blocklyCanvasTransitioning'); - this.svgBubbleCanvas_.classList.remove('blocklyCanvasTransitioning'); + dom.removeClass(this.svgBlockCanvas_, 'blocklyCanvasTransitioning'); + dom.removeClass(this.svgBubbleCanvas_, 'blocklyCanvasTransitioning'); } /** Center the workspace. */ diff --git a/tests/mocha/toolbox_test.js b/tests/mocha/toolbox_test.js index 40007651c..45dfe0b85 100644 --- a/tests/mocha/toolbox_test.js +++ b/tests/mocha/toolbox_test.js @@ -130,6 +130,26 @@ suite('Toolbox', function() { this.toolbox.render(jsonDef); chai.assert.lengthOf(this.toolbox.contents_, 1); }); + test('multiple icon classes can be applied', function() { + const jsonDef = {'contents': [ + { + "kind": "category", + "cssConfig": { + "icon": "customIcon customIconEvents", + }, + "contents": [ + { + "kind": "block", + "blockxml": 'FirstCategory-FirstBlock', + }, + ], + }, + ]}; + chai.assert.doesNotThrow(() => { + this.toolbox.render(jsonDef); + }); + chai.assert.lengthOf(this.toolbox.contents_, 1); + }); }); suite('onClick_', function() {