UNPKG

veritatisquam

Version:
491 lines (436 loc) 14.5 kB
// Internal Imports import { Component } from '../component'; import { Events, Roles, ToolbarControlTypes } from '../../interfaces'; import { Tools } from '../../tools'; import { DOMUtils } from '../../services'; import * as Configuration from '../../configuration'; // D3 Imports import { event, select } from 'd3-selection'; // import the settings for the css prefix import settings from 'carbon-components/es/globals/js/settings'; export class Toolbar extends Component { type = 'toolbar'; // overflow menu button to control background color overflowButton: any; // ul options list element overflowMenu: any; // x, y coordinate of overflow menu overflowMenuX = 0; overflowMenuY = 0; // Use a random number to create overflow menu item unique ID // so they don't interfere the other overflow menu item in a page overflowMenuItemId = Math.floor(Math.random() * 99999999999); init() { const options = this.getOptions(); // Grab the tooltip element const holder = select(this.services.domUtils.getHolder()); const chartPrefix = Tools.getProperty(options, 'style', 'prefix'); this.overflowMenu = DOMUtils.appendOrSelect( holder, `div.${settings.prefix}--${chartPrefix}--overflowMenu` ); this.overflowMenu.style('max-width', null); // listen to show overflow menu event to render the overflow menu this.services.events.addEventListener( Events.Toolbar.SHOW_OVERFLOW_MENU, () => { this.overflowMenu.html(this.getOverflowMenuHTML()); } ); // listen to hide overflow menu event to hide the overflow menu this.services.events.addEventListener( Events.Toolbar.HIDE_OVERFLOW_MENU, () => { this.overflowMenu.html(null); } ); // hide overflow menu if user clicks on somewhere in web page document.body.addEventListener('click', () => this.updateOverflowMenu(false) ); } render(animate = true) { const isDataLoading = Tools.getProperty( this.getOptions(), 'data', 'loading' ); // size of toolbar button with background const buttonSize = Configuration.toolbar.buttonSize; const parentY = parseFloat(this.parent.node().getAttribute('y')); const svg = this.getContainerSVG(); // TODO -- adjust toolbar Y position to align title component // before layout component supports vertical alignment center const Y_OFFSET = -6; svg.attr('transform', `translate(0, ${parentY + Y_OFFSET})`); const { width } = DOMUtils.getSVGElementSize( this.services.domUtils.getMainSVG(), { useAttrs: true, } ); // overflow menu width is 160px // it's set by Carbon component const overflowMenuWidth = 160; // no good solution to get correct Toolbar position // parent x doesn't work well // assume the overflow icon has right alignment in layout this.overflowMenuX = width - overflowMenuWidth; this.overflowMenuY = parentY + Y_OFFSET + buttonSize; const container = DOMUtils.appendOrSelect(svg, 'svg.toolbar-container') .attr('width', '100%') .attr('height', Configuration.toolbar.height) .attr('opacity', 1); // clean children first container.html(null); // get the toolbar buttons const { buttonList, overflowMenuItemList } = this.getControlConfigs(); // overflow button is required only if overflow menu item list is valid if (!!overflowMenuItemList) { buttonList.push(this.getOverflowButtonConfig()); } // loading or empty state if (isDataLoading || buttonList.length === 0) { // put an empty rect to keep space unchanged DOMUtils.appendOrSelect(container, 'svg.toolbar-loading-spacer') .append('rect') .attr('height', Configuration.toolbar.height) .attr('width', buttonSize * 3) // value doesn't matter but can't be empty .attr('opacity', 0); } else { // render toolbar buttons sequentially let buttonXPosition = 0; buttonList.forEach((button) => { // button container const buttonContainer = DOMUtils.appendOrSelect( container, `svg.${button.id}` ).classed('toolbar-button', true); // add button background rect const buttonBackground = DOMUtils.appendOrSelect( buttonContainer, 'rect.toolbar-button-background' ) .attr('x', buttonXPosition) .attr('y', 0) .attr('width', buttonSize) .attr('height', buttonSize); const buttonIcon = DOMUtils.appendOrSelect( buttonContainer, 'svg.toolbar-button-icon' ) .attr( 'x', buttonXPosition + Configuration.toolbar.iconPadding ) .attr('y', Configuration.toolbar.iconPadding) .attr('width', Configuration.toolbar.iconSize) .attr('height', Configuration.toolbar.iconSize) .attr('viewBox', '0 0 32 32') .attr('role', Roles.IMG); buttonIcon.html(button.iconSVGContent); if (button.shouldBeDisabled()) { buttonContainer .classed('toolbar-button--disabled', true) .classed('toolbar-button--focusable', false) .attr('tabindex', -1) .attr('role', null); buttonIcon.classed('toolbar-button-icon--disabled', true); buttonBackground.classed( 'toolbar-button-background--disabled', true ); buttonContainer.on('click', null).on('keyup', null); } else { buttonContainer .classed('toolbar-button--disabled', false) .classed('toolbar-button--focusable', true) .attr('tabindex', 0) .attr('role', Roles.BUTTON); buttonIcon.classed('toolbar-button-icon--disabled', false); buttonBackground.classed( 'toolbar-button-background--disabled', false ); buttonContainer .on('click', button.clickFunction) .on('keyup', () => { if ( (event.key && event.key === 'Enter') || event.key === ' ' ) { event.preventDefault(); button.clickFunction(); } }); } buttonXPosition += buttonSize; }); this.overflowButton = this.getContainerSVG().select( 'svg.toolbar-overflow-menu' ); if (this.isOverflowMenuOpen()) { // keep overflow menu displayed this.updateOverflowMenu(true); } } } isOverflowMenuOpen() { return ( this.overflowMenu .selectAll('ul.bx--overflow-menu-options--open') .size() > 0 ); } // show/hide overflow menu updateOverflowMenu(show: boolean) { // update overflow button background if (this.overflowButton) { this.overflowButton.classed('toolbar-button--hovered', show); } if (show) { this.services.events.dispatchEvent( Events.Toolbar.SHOW_OVERFLOW_MENU ); } else { this.services.events.dispatchEvent( Events.Toolbar.HIDE_OVERFLOW_MENU ); } } focusOnPreviousEnabledMenuItem(currentItemIndex) { const overflowMenuItems = this.getOverflowMenuItems(); let previousItemIndex = overflowMenuItems.length; for (let i = currentItemIndex - 1; i >= 0; i--) { const previousOverflowMenuItem = overflowMenuItems[i]; if (!previousOverflowMenuItem.shouldBeDisabled()) { previousItemIndex = i; break; } } // only if previous enabled menu item found if (previousItemIndex < overflowMenuItems.length) { const previousItemNode = select( '#' + overflowMenuItems[previousItemIndex].id + this.overflowMenuItemId ).node(); if ('focus' in previousItemNode) { previousItemNode.focus(); } } } focusOnNextEnabledMenuItem(currentItemIndex) { const overflowMenuItems = this.getOverflowMenuItems(); let nextItemIndex = -1; for (let i = currentItemIndex + 1; i < overflowMenuItems.length; i++) { const nextOverflowMenuItem = overflowMenuItems[i]; if (!nextOverflowMenuItem.shouldBeDisabled()) { nextItemIndex = i; break; } } // only if next enabled menu item found if (nextItemIndex > -1) { const nextItemNode = select( '#' + overflowMenuItems[nextItemIndex].id + this.overflowMenuItemId ).node(); if ('focus' in nextItemNode) { nextItemNode.focus(); } } } toggleOverflowMenu() { if (this.isOverflowMenuOpen()) { // hide overflow menu this.updateOverflowMenu(false); } else { // show overflow menu this.updateOverflowMenu(true); // setup overflow menu item event listener const self = this; const overflowMenuItems = this.getOverflowMenuItems(); overflowMenuItems.forEach((menuItem, index) => { const element = select( '#' + menuItem.id + this.overflowMenuItemId ); if (element !== null) { element.on('click', () => { // call the specified function menuItem.clickFunction(); // hide overflow menu self.updateOverflowMenu(false); }); element.on('keyup', () => { if (event.key === 'Enter') { // call the specified function menuItem.clickFunction(); } else if (event.key === 'ArrowUp') { // focus on previous menu item self.focusOnPreviousEnabledMenuItem(index); } else if (event.key === 'ArrowDown') { // focus on next menu item self.focusOnNextEnabledMenuItem(index); } // Not hide overflow menu by keyboard arrow up/down event }); } }); // default to focus on the first enabled menu item self.focusOnNextEnabledMenuItem(-1); } event.stopImmediatePropagation(); } getOverflowMenuHTML() { const overflowMenuItems = this.getOverflowMenuItems(); // don't render whole overflow menu if no overflow menu item if (!overflowMenuItems || overflowMenuItems.length === 0) { return ''; } let overflowMenuHtml; overflowMenuHtml = `<div data-floating-menu-container="true" data-floating-menu-direction="bottom" role="main"> <ul class="bx--overflow-menu-options bx--overflow-menu--flip bx--overflow-menu-options--open" tabindex="-1" role="${Roles.MENU}" aria-label="Menu" data-floating-menu-direction="bottom" style="left:${this.overflowMenuX}px; top:${this.overflowMenuY}px;">`; // generate html for each overflow menu items overflowMenuItems.forEach((menuItem, index) => { const menuItemClasses = 'bx--overflow-menu-options__option'.concat( menuItem.shouldBeDisabled() ? ' bx--overflow-menu-options__option--disabled' // class for disabled menu item : '' ); overflowMenuHtml += `<li class="${menuItemClasses}" role="${ Roles.MENU_ITEM }"> <button class="bx--overflow-menu-options__btn" data-floating-menu-primary-focus="${index === 0}" tabindex="-1" index="${index}" title="${menuItem.text}" id="${menuItem.id + this.overflowMenuItemId}"> <div class="bx--overflow-menu-options__option-content"> ${menuItem.text} </div> </button> </li>`; }); overflowMenuHtml += `</ul></div>`; return overflowMenuHtml; } getControlConfigs() { const numberOfIcons = Tools.getProperty( this.getOptions(), 'toolbar', 'numberOfIcons' ); const controls = Tools.getProperty( this.getOptions(), 'toolbar', 'controls' ); const controlList = []; controls.forEach((control) => { const controlConfig = this.getControlConfigByType(control.type); // add to list if config is valid if (controlConfig) { controlConfig.text = control.text ? control.text : control.type; controlList.push(controlConfig); } }); if (controlList.length <= numberOfIcons) { return { buttonList: controlList, }; } return { // leave one button for overflow button buttonList: controlList.splice(0, numberOfIcons - 1), overflowMenuItemList: controlList, }; } getOverflowMenuItems() { const { overflowMenuItemList } = this.getControlConfigs(); if (!!overflowMenuItemList) { return overflowMenuItemList; } else { return []; } } // special button config for overflow button getOverflowButtonConfig() { return { id: 'toolbar-overflow-menu', shouldBeDisabled: () => false, iconSVGContent: `<circle cx="16" cy="8" r="2"></circle> <circle cx="16" cy="16" r="2"></circle> <circle cx="16" cy="24" r="2"></circle>`, clickFunction: () => this.toggleOverflowMenu(), }; } getControlConfigByType(controlType: ToolbarControlTypes) { const isZoomBarEnabled = this.services.zoom.isZoomBarEnabled() && !this.services.zoom.isEmptyState(); let controlConfig; switch (controlType) { case ToolbarControlTypes.ZOOM_IN: if (isZoomBarEnabled) { controlConfig = { id: 'toolbar-zoomIn', shouldBeDisabled: () => this.services.zoom.isMinZoomDomain(), iconSVGContent: this.getControlIconByType(controlType), clickFunction: () => this.services.zoom.zoomIn(), }; } break; case ToolbarControlTypes.ZOOM_OUT: if (isZoomBarEnabled) { controlConfig = { id: 'toolbar-zoomOut', shouldBeDisabled: () => this.services.zoom.isMaxZoomDomain(), iconSVGContent: this.getControlIconByType(controlType), clickFunction: () => this.services.zoom.zoomOut(), }; } break; case ToolbarControlTypes.RESET_ZOOM: if (isZoomBarEnabled) { controlConfig = { id: 'toolbar-resetZoom', shouldBeDisabled: () => this.services.zoom.isMaxZoomDomain(), iconSVGContent: this.getControlIconByType(controlType), clickFunction: () => this.services.zoom.resetZoomDomain(), }; } break; // add more toolbar control configuration here default: throw Error( 'Not supported toolbar control type: ' + controlType ); } return controlConfig; } getControlIconByType(controlType: ToolbarControlTypes) { switch (controlType) { case ToolbarControlTypes.ZOOM_IN: return `<polygon points="19 13 15 13 15 9 13 9 13 13 9 13 9 15 13 15 13 19 15 19 15 15 19 15 19 13"/> <path d="M22.45,21A10.87,10.87,0,0,0,25,14,11,11,0,1,0,14,25a10.87,10.87,0,0,0,7-2.55L28.59,30,30,28.59ZM14,23a9,9,0,1,1,9-9A9,9,0,0,1,14,23Z"/>`; case ToolbarControlTypes.ZOOM_OUT: return `<rect x="9" y="13" width="10" height="2"/> <path d="M22.45,21A10.87,10.87,0,0,0,25,14,11,11,0,1,0,14,25a10.87,10.87,0,0,0,7-2.55L28.59,30,30,28.59ZM14,23a9,9,0,1,1,9-9A9,9,0,0,1,14,23Z"/>`; case ToolbarControlTypes.RESET_ZOOM: return `<path d="M22.4478,21A10.855,10.855,0,0,0,25,14,10.99,10.99,0,0,0,6,6.4658V2H4v8h8V8H7.332a8.9768,8.9768,0,1,1-2.1,8H3.1912A11.0118,11.0118,0,0,0,14,25a10.855,10.855,0,0,0,7-2.5522L28.5859,30,30,28.5859Z"/>`; // add more icons here // svg icon must be with 32x32 viewBox default: throw Error( 'Not supported toolbar control type: ' + controlType ); } } }