UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

304 lines • 13.5 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2022 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.SelectComponent = exports.SELECT_COMPONENT_CONTAINER = void 0; const React = require("react"); const ReactDOM = require("react-dom"); const DOMPurify = require("dompurify"); const widget_1 = require("./widget"); const browser_1 = require("../browser"); require("../../../src/browser/style/select-component.css"); exports.SELECT_COMPONENT_CONTAINER = 'select-component-container'; class SelectComponent extends React.Component { constructor(props) { super(props); this.fieldRef = React.createRef(); this.dropdownRef = React.createRef(); this.mountedListeners = new Map(); this.optimalWidth = 0; this.optimalHeight = 0; let selected = 0; if (typeof props.defaultValue === 'number') { selected = props.defaultValue; } else if (typeof props.defaultValue === 'string') { selected = Math.max(props.options.findIndex(e => e.value === props.defaultValue), 0); } this.state = { selected, original: selected, hover: selected }; let list = document.getElementById(exports.SELECT_COMPONENT_CONTAINER); if (!list) { list = document.createElement('div'); list.id = exports.SELECT_COMPONENT_CONTAINER; document.body.appendChild(list); } this.dropdownElement = list; } get options() { return this.props.options; } get value() { var _a; return (_a = this.props.options[this.state.selected].value) !== null && _a !== void 0 ? _a : this.state.selected; } set value(value) { let index = -1; if (typeof value === 'number') { index = value; } else if (typeof value === 'string') { index = this.props.options.findIndex(e => e.value === value); } if (index >= 0) { this.setState({ selected: index, original: index, hover: index }); } } getOptimalWidth() { const textWidth = (0, browser_1.measureTextWidth)(this.props.options.map(e => e.label || e.value || '' + (e.detail || ''))); return Math.ceil(textWidth + 16); } getOptimalHeight(maxWidth) { const firstLine = this.props.options.find(e => e.label || e.value || e.detail); if (!firstLine) { return 0; } if (maxWidth) { maxWidth = Math.ceil(maxWidth) + 10; // Increase width by 10 due to side padding } const descriptionHeight = (0, browser_1.measureTextHeight)(this.props.options.map(e => e.description || ''), { maxWidth: `${maxWidth}px` }) + 18; const singleLineHeight = (0, browser_1.measureTextHeight)(firstLine.label || firstLine.value || firstLine.detail || '') + 6; const optimal = descriptionHeight + singleLineHeight * this.props.options.length; return optimal + 20; // Just to be safe, add another 20 pixels here } attachListeners() { var _a; const hide = (event) => { var _a; if (!((_a = this.dropdownRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target))) { this.hide(); } }; this.mountedListeners.set('scroll', hide); this.mountedListeners.set('wheel', hide); let parent = (_a = this.fieldRef.current) === null || _a === void 0 ? void 0 : _a.parentElement; while (parent) { // Workaround for perfect scrollbar, since using `overflow: hidden` // neither triggers the `scroll`, `wheel` nor `blur` event if (parent.classList.contains('ps')) { parent.addEventListener('ps-scroll-y', hide); } parent = parent.parentElement; } for (const [key, listener] of this.mountedListeners.entries()) { window.addEventListener(key, listener); } } componentWillUnmount() { var _a; if (this.mountedListeners.size > 0) { const eventListener = this.mountedListeners.get('scroll'); let parent = (_a = this.fieldRef.current) === null || _a === void 0 ? void 0 : _a.parentElement; while (parent) { parent.removeEventListener('ps-scroll-y', eventListener); parent = parent.parentElement; } for (const [key, listener] of this.mountedListeners.entries()) { window.removeEventListener(key, listener); } } } render() { var _a, _b; const { options } = this.props; let { selected } = this.state; if ((_a = options[selected]) === null || _a === void 0 ? void 0 : _a.separator) { selected = this.nextNotSeparator('forwards'); } const selectedItemLabel = (_b = options[selected].label) !== null && _b !== void 0 ? _b : options[selected].value; return React.createElement(React.Fragment, null, React.createElement("div", { key: "select-component", ref: this.fieldRef, tabIndex: 0, className: "theia-select-component", onClick: e => this.handleClickEvent(e), onBlur: () => { var _a, _b; this.hide(); (_b = (_a = this.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a); }, onFocus: () => { var _a, _b; return (_b = (_a = this.props).onFocus) === null || _b === void 0 ? void 0 : _b.call(_a); }, onKeyDown: e => this.handleKeypress(e) }, React.createElement("div", { key: "label", className: "theia-select-component-label" }, selectedItemLabel), React.createElement("div", { key: "icon", className: `theia-select-component-chevron ${(0, widget_1.codicon)('chevron-down')}` })), ReactDOM.createPortal(this.renderDropdown(), this.dropdownElement)); } nextNotSeparator(direction) { var _a; const { options } = this.props; const step = direction === 'forwards' ? 1 : -1; const length = this.props.options.length; let selected = this.state.selected; let count = 0; do { selected = (selected + step) % length; if (selected < 0) { selected = length - 1; } count++; } while (((_a = options[selected]) === null || _a === void 0 ? void 0 : _a.separator) && count < length); return selected; } handleKeypress(ev) { if (!this.fieldRef.current) { return; } if (ev.key === 'ArrowUp') { const selected = this.nextNotSeparator('backwards'); this.setState({ selected, hover: selected }); } else if (ev.key === 'ArrowDown') { if (this.state.dimensions) { const selected = this.nextNotSeparator('forwards'); this.setState({ selected, hover: selected }); } else { this.toggleVisibility(); this.setState({ selected: 0, hover: 0, }); } } else if (ev.key === 'Enter') { if (!this.state.dimensions) { this.toggleVisibility(); } else { const selected = this.state.selected; this.selectOption(selected, this.props.options[selected]); } } else if (ev.key === 'Escape' || ev.key === 'Tab') { this.hide(); } ev.stopPropagation(); ev.nativeEvent.stopImmediatePropagation(); } handleClickEvent(event) { this.toggleVisibility(); event.stopPropagation(); event.nativeEvent.stopImmediatePropagation(); } toggleVisibility() { if (!this.fieldRef.current) { return; } if (!this.state.dimensions) { const rect = this.fieldRef.current.getBoundingClientRect(); this.setState({ dimensions: rect }); } else { this.hide(); } } hide(index) { const selectedIndex = index === undefined ? this.state.original : index; this.setState({ dimensions: undefined, selected: selectedIndex, original: selectedIndex, hover: selectedIndex }); } renderDropdown() { if (!this.state.dimensions) { return; } if (this.mountedListeners.size === 0) { // Only attach our listeners once we render our dropdown menu this.attachListeners(); // We can now also calculate the optimal width this.optimalWidth = this.getOptimalWidth(); this.optimalHeight = this.getOptimalHeight(Math.max(this.state.dimensions.width, this.optimalWidth)); } const clientRect = document.getElementById('theia-app-shell').getBoundingClientRect(); const availableTop = this.state.dimensions.top - clientRect.top; const availableBottom = clientRect.top + clientRect.height - this.state.dimensions.bottom; // prefer rendering to the bottom unless there is not enough space and more content can be shown to the top const invert = availableBottom < this.optimalHeight && (availableBottom - this.optimalHeight) < (availableTop - this.optimalHeight); const { options } = this.props; const { hover } = this.state; const description = options[hover].description; const markdown = options[hover].markdown; const items = options.map((item, i) => this.renderOption(i, item)); if (description) { let descriptionNode; const className = 'theia-select-component-description'; if (markdown) { descriptionNode = React.createElement("div", { key: "description", className: className, dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(description) } }); // eslint-disable-line react/no-danger } else { descriptionNode = React.createElement("div", { key: "description", className: className }, description); } if (invert) { items.unshift(descriptionNode); } else { items.push(descriptionNode); } } const calculatedWidth = Math.max(this.state.dimensions.width, this.optimalWidth); const maxWidth = clientRect.width - this.state.dimensions.left; return React.createElement("div", { key: "dropdown", className: "theia-select-component-dropdown", style: { top: invert ? 'none' : this.state.dimensions.bottom, bottom: invert ? clientRect.top + clientRect.height - this.state.dimensions.top : 'none', left: this.state.dimensions.left, width: Math.min(calculatedWidth, maxWidth), maxHeight: clientRect.height - (invert ? clientRect.height - this.state.dimensions.bottom : this.state.dimensions.top) - this.state.dimensions.height, position: 'absolute' }, ref: this.dropdownRef }, items); } renderOption(index, option) { var _a; if (option.separator) { return React.createElement("div", { key: index, className: "theia-select-component-separator" }); } const selected = this.state.hover; return (React.createElement("div", { key: index, className: `theia-select-component-option${index === selected ? ' selected' : ''}`, onMouseOver: () => { this.setState({ hover: index }); }, onMouseDown: () => { this.selectOption(index, option); } }, React.createElement("div", { key: "value", className: "theia-select-component-option-value" }, (_a = option.label) !== null && _a !== void 0 ? _a : option.value), option.detail && React.createElement("div", { key: "detail", className: "theia-select-component-option-detail" }, option.detail))); } selectOption(index, option) { var _a, _b; (_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, option, index); this.hide(index); } } exports.SelectComponent = SelectComponent; //# sourceMappingURL=select-component.js.map