@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
304 lines • 13.5 kB
JavaScript
"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