UNPKG

dmn-js-shared

Version:

Shared components used by dmn-js

225 lines (218 loc) 5.9 kB
import { createVNode } from "inferno"; import { Component } from 'inferno'; import escapeHtml from 'escape-html'; import { getRange, setRange, applyRange, getWindowSelection } from 'selection-ranges'; import selectionUpdate from 'selection-update'; /** * A content ediable that performs proper selection updates on * editable changes. It normalizes editor operations by allowing * only <br/> and plain text to be inserted. * * The callback `onInput(text)` recieves text (including line breaks) * only. Updating the value via props will update the selection * if needed, too. * * @example * * class SomeComponent extends Component { * * render() { * return ( * <ContentEditable * className="some classes" * value={ this.state.text } * onInput={ this.handleInput } * onChange={ this.handleChange } * onFocus={ ... } * onBlur={ ... } /> * ); * } * * } * */ export default class ContentEditable extends Component { constructor(props, context) { super(props, context); this.state = {}; // TODO(nikku): remove once we drop IE 11 support if (isIE()) { // onInput shim for IE <= 11 this.onInputIEPolyfill = event => { var oldText = this.node.innerHTML; setTimeout(() => { var text = this.node.innerHTML; if (oldText !== text) { this.onInput(event); } }, 0); }; } } componentWillUpdate(newProps, newState) { // save old selection + text for later var node = this.node; var range = newState.focussed && getRange(node); this.selected = range && { range: range, text: innerText(node) }; } componentDidUpdate() { var selected = this.selected; if (!selected) { return; } // compute and restore selection based on // (possibly new) text const range = selected.range; const text = selected.text; const node = this.node; const newText = innerText(node); const newRange = newText !== text ? selectionUpdate(range, text, newText) : range; setRange(node, newRange); } onFocus = event => { var propsFocus = this.props.onFocus; this.setState({ focussed: true }); if (typeof propsFocus === 'function') { propsFocus(event); } }; onBlur = event => { const { onBlur, onChange, value } = this.props; this.setState({ focussed: false }); if (typeof onChange === 'function' && this.node) { const currentValue = innerText(this.node); if (currentValue !== value) { onChange(currentValue); } } if (typeof onBlur === 'function') { onBlur(event); } }; onkeydown = event => { // enter if (event.which === 13) { // prevent default action (<br/> insert) event.preventDefault(); if (this.props.ctrlForNewline && !isCmd(event)) { return; } if (this.props.singleLine) { return; } event.stopPropagation(); insertLineBreak(); this.onInput(event); } }; onInput = event => { var propsInput = this.props.onInput; if (typeof propsInput !== 'function') { return; } var text = innerText(this.node); propsInput(text); }; // TODO(barmac): remove once we drop IE 11 support onkeypress = event => { if (this.onInputIEPolyfill) { this.onInputIEPolyfill(event); } }; onPaste = event => { // TODO(barmac): remove once we drop IE 11 support if (this.onInputIEPolyfill) { this.onInputIEPolyfill(event); } if (this.props.singleLine) { const text = (event.clipboardData || window.clipboardData).getData('text'); // replace newline with space document.execCommand('insertText', false, text.replace(/\n/g, ' ')); event.preventDefault(); } }; getClassName() { const { className, placeholder, value } = this.props; return [className || '', 'content-editable', !value && placeholder ? 'placeholder' : ''].join(' '); } render(props) { var { label, value, placeholder } = props; // QUIRK: must add trailing <br/> for line // breaks to properly work value = escapeHtml(value).replace(/\r?\n/g, '<br/>') + '<br/>'; return createVNode(1, "div", this.getClassName(), null, 1, { "aria-label": label, "role": "textbox", "aria-multiline": !this.props.singleLine, "tabIndex": "0", "contentEditable": "true", "spellCheck": "false", "data-placeholder": placeholder || '', "onInput": this.onInput, "onkeypress": this.onkeypress, "onPaste": this.onPaste, "onFocus": this.onFocus, "onBlur": this.onBlur, "onkeydown": this.onkeydown, "dangerouslySetInnerHTML": { __html: value } }, null, node => this.node = node); } } function brTag() { return document.createElement('br'); } function innerText(node) { // QUIRK: we must remove the last trailing <br/>, if any return node.innerText.replace(/\n$/, ''); } function insertLineBreak() { // insert line break at current insertation // point; this assumes that the correct element, i.e. // a <ContentEditable /> is currently focussed var selection = getWindowSelection(); var range = selection.getRangeAt(0); if (!range) { return; } var newRange = range.cloneRange(); var br = brTag(); newRange.deleteContents(); newRange.insertNode(br); newRange.setStartAfter(br); newRange.setEndAfter(br); applyRange(newRange); } function isIE() { var ua = window.navigator.userAgent; return ( // IE 10 or older ua.indexOf('MSIE ') > 0 || // IE 11 ua.indexOf('Trident/') > 0 ); } function isCmd(event) { return event.metaKey || event.ctrlKey; } //# sourceMappingURL=ContentEditable.js.map