UNPKG

dmn-js-shared

Version:

Shared components used by dmn-js

272 lines (265 loc) 7.41 kB
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