dmn-js-shared
Version:
Shared components used by dmn-js
272 lines (265 loc) • 7.41 kB
JavaScript
import { createVNode } from "inferno";
import { Component, createPortal } from 'inferno';
import { inject } from 'table-js/lib/components';
import { assign } from 'min-dash';
import { domify, remove as domRemove } from 'min-dom';
export default class InputSelect extends Component {
constructor(props, context) {
super(props, context);
inject(this);
const {
value
} = props;
this.state = {
value,
optionsVisible: false
};
this._portalEl = null;
}
componentDidMount() {
document.addEventListener('mousedown', this.onGlobalClick);
document.addEventListener('focusin', this.onFocusChanged);
this.keyboard.addListener(this.onKeyboard);
}
componentWillUnmount() {
document.removeEventListener('focusin', this.onFocusChanged);
document.removeEventListener('mousedown', this.onGlobalClick);
this.keyboard.removeListener(this.onKeyboard);
this.removePortalEl();
}
componentWillReceiveProps(props) {
const {
value
} = props;
this.setState({
value
});
}
componentWillUpdate(nextProps, nextState) {
const {
optionsVisible
} = nextState;
if (optionsVisible) {
if (!this._portalEl) {
this.addPortalEl();
}
} else {
if (this._portalEl) {
this.removePortalEl();
}
}
}
componentDidUpdate() {
const {
optionsVisible
} = this.state;
if (!optionsVisible || !this.inputNode) {
return;
}
const optionsBounds = this.getOptionsBounds();
assign(this._portalEl.style, optionsBounds);
}
getOptionsBounds() {
const container = this.renderer.getContainer();
const {
top: containerTop,
left: containerLeft,
bottom: containerBottom
} = container.getBoundingClientRect();
const {
top: inputTop,
left: inputLeft,
width,
height,
bottom: inputBottom
} = this.inputNode.getBoundingClientRect();
const top = inputTop + height - containerTop + container.scrollTop;
const left = inputLeft - containerLeft + container.scrollLeft;
const bounds = {
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
'max-height': `calc(100% - ${top}px)`
};
// open the options upwards when not even one option (=input height) fits
if (containerBottom - inputBottom < height) {
const bottom = containerBottom - inputTop;
bounds.bottom = `${bottom}px`;
bounds['max-height'] = `calc(100% - ${bottom})`;
delete bounds.top;
}
return bounds;
}
addPortalEl() {
this._portalEl = domify('<div class="dms-select-options"></div>');
const container = this.renderer.getContainer();
container.appendChild(this._portalEl);
// suppress mousedown event propagation to handle click events inside the component
this._portalEl.addEventListener('mousedown', stopPropagation);
}
removePortalEl() {
if (this._portalEl) {
this._portalEl.removeEventListener('mousedown', stopPropagation);
domRemove(this._portalEl);
this._portalEl = null;
}
}
onChange = value => {
this.setState({
value
});
const {
onChange
} = this.props;
if (typeof onChange !== 'function') {
return;
}
onChange(value);
};
onInputClick = event => {
event.preventDefault();
event.stopPropagation();
this.setOptionsVisible(!this.state.optionsVisible);
this.focusInput();
};
onInput = event => {
const {
value
} = event.target;
this.onChange(value);
};
onOptionClick = (value, event) => {
event.preventDefault();
event.stopPropagation();
this.setOptionsVisible(false);
this.onChange(value);
this.focusInput();
};
/**
* Focus input node
*/
focusInput() {
const node = this.inputNode;
node.focus();
// move cursor to end of input
if ('selectionStart' in node) {
node.selectionStart = 100000;
}
}
checkClose(focusTarget) {
if (this._portalEl && !this._portalEl.contains(focusTarget) && !this.parentNode.contains(focusTarget)) {
this.setOptionsVisible(false);
}
}
onFocusChanged = evt => {
this.checkClose(evt.target);
};
onGlobalClick = evt => {
this.checkClose(evt.target);
};
select(direction) {
const {
options
} = this.props;
const {
value
} = this.state;
if (!options) {
return;
}
const option = options.filter(o => o.value === value)[0];
const idx = option ? options.indexOf(option) : -1;
const nextIdx = idx === -1 ? direction === 1 ? 0 : options.length - 1 : (idx + direction) % options.length;
const nextOption = options[nextIdx < 0 ? options.length + nextIdx : nextIdx];
this.onChange(nextOption.value);
}
setOptionsVisible(optionsVisible) {
this.setState({
optionsVisible
});
}
onKeyDown = evt => {
const {
optionsVisible
} = this.state;
var code = evt.which;
// DOWN or UP
if (code === 40 || code === 38) {
evt.stopPropagation();
evt.preventDefault();
if (!optionsVisible) {
this.setOptionsVisible(true);
} else {
this.select(code === 40 ? 1 : -1);
}
}
if (optionsVisible) {
// ENTER
// ESC
if (code === 13 || code === 27) {
evt.stopPropagation();
evt.preventDefault();
this.setOptionsVisible(false);
}
}
};
onKeyboard = keycode => {
const {
optionsVisible
} = this.state;
if (!optionsVisible) {
return;
}
// close on ESC
if (keycode === 27) {
this.setOptionsVisible(false);
return true;
}
};
renderOptions(options, activeOption) {
return createVNode(1, "div", "options", options.map(option => {
return createVNode(1, "div", ['option', activeOption === option ? 'active' : ''].join(' '), option.label, 0, {
"data-value": option.value,
"onClick": e => this.onOptionClick(option.value, e)
});
}), 0);
}
render() {
const {
className,
label: inputLabel,
id,
options,
noInput,
title
} = this.props;
const {
optionsVisible,
value
} = this.state;
const option = options ? options.filter(o => o.value === value)[0] : false;
const label = option ? option.label : value;
return createVNode(1, "div", [className || '', 'dms-input-select'].join(' '), [noInput ? createVNode(1, "div", "dms-input", label, 0, {
"aria-label": inputLabel,
"tabIndex": "0",
"onKeyDown": this.onKeyDown
}, null, node => this.inputNode = node) : createVNode(64, "input", "dms-input", null, 1, {
"aria-label": inputLabel,
"onInput": this.onInput,
"onKeyDown": this.onKeyDown,
"spellCheck": "false",
"type": "text",
"value": value,
"id": id
}, null, node => this.inputNode = node), createVNode(1, "span", ['dms-input-select-icon', optionsVisible ? 'dmn-icon-up' : 'dmn-icon-down'].join(' ')), optionsVisible && createPortal(this.renderOptions(options, option), this._portalEl)], 0, {
"title": title,
"onClick": this.onInputClick
}, null, node => this.parentNode = node);
}
}
InputSelect.$inject = ['keyboard', 'renderer'];
// helper ////
function stopPropagation(event) {
event.stopPropagation();
}
//# sourceMappingURL=InputSelect.js.map