diff --git a/packages/dmn-js-decision-table/assets/css/dmn-js-decision-table-controls.css b/packages/dmn-js-decision-table/assets/css/dmn-js-decision-table-controls.css
index a68cd7716..d5260fbca 100644
--- a/packages/dmn-js-decision-table/assets/css/dmn-js-decision-table-controls.css
+++ b/packages/dmn-js-decision-table/assets/css/dmn-js-decision-table-controls.css
@@ -251,3 +251,23 @@
width: 20px;
right: 0;
}
+
+/* cell editor */
+.dmn-decision-table-container .cell-editor__placeholder {
+ position: absolute;
+}
+
+.dmn-decision-table-container .cell-editor:focus-within .cell-editor__placeholder,
+.dmn-decision-table-container .cell-editor:focus-within .dmn-expression-language {
+ display: none;
+}
+
+.dmn-decision-table-container .cell-editor,
+.dmn-decision-table-container .cell-editor .cm-scroller {
+ line-height: 1;
+ font-family: monospace;
+}
+
+.dmn-decision-table-container .cell-editor .feel-editor.focussed > :nth-child(2) {
+ display: none;
+}
diff --git a/packages/dmn-js-decision-table/src/features/cell-selection/CellSelectionUtil.js b/packages/dmn-js-decision-table/src/features/cell-selection/CellSelectionUtil.js
index 92d406b44..ebfa83eb5 100644
--- a/packages/dmn-js-decision-table/src/features/cell-selection/CellSelectionUtil.js
+++ b/packages/dmn-js-decision-table/src/features/cell-selection/CellSelectionUtil.js
@@ -7,7 +7,6 @@ import {
import cssEscape from 'css.escape';
import {
- setRange,
getRange
} from 'selection-ranges';
@@ -109,6 +108,6 @@ export function ensureFocus(el) {
const range = getRange(focusEl);
if (!range || range.end === 0) {
- setRange(focusEl, { start: 5000, end: 5000 });
+ window.getSelection().setPosition(focusEl.firstChild, focusEl.firstChild.length);
}
}
\ No newline at end of file
diff --git a/packages/dmn-js-decision-table/src/features/decision-rules/components/DecisionRulesCellEditorComponent.js b/packages/dmn-js-decision-table/src/features/decision-rules/components/DecisionRulesCellEditorComponent.js
index 794a6c3c1..8ec21ee5d 100644
--- a/packages/dmn-js-decision-table/src/features/decision-rules/components/DecisionRulesCellEditorComponent.js
+++ b/packages/dmn-js-decision-table/src/features/decision-rules/components/DecisionRulesCellEditorComponent.js
@@ -4,7 +4,8 @@ import { isString } from 'min-dash';
import { is } from 'dmn-js-shared/lib/util/ModelUtil';
-import EditableComponent from 'dmn-js-shared/lib/components/EditableComponent';
+import ContentEditable from 'dmn-js-shared/lib/components/ContentEditable';
+import LiteralExpression from 'dmn-js-shared/lib/components/LiteralExpression';
import { Cell } from 'table-js/lib/components';
@@ -14,22 +15,14 @@ export default class DecisionRulesCellEditorComponent extends Component {
constructor(props, context) {
super(props, context);
- this.state = {
- isFocussed: false
- };
-
this.changeCellValue = this.changeCellValue.bind(this);
- this.onFocus = this.onFocus.bind(this);
- this.onBlur = this.onBlur.bind(this);
this.onElementsChanged = this.onElementsChanged.bind(this);
}
-
onElementsChanged() {
this.forceUpdate();
}
-
componentWillMount() {
const { injector } = this.context;
@@ -42,35 +35,18 @@ export default class DecisionRulesCellEditorComponent extends Component {
changeSupport.onElementsChanged(cell.id, this.onElementsChanged);
}
-
componentWillUnmount() {
const { cell } = this.props;
this._changeSupport.offElementsChanged(cell.id, this.onElementsChanged);
}
-
changeCellValue(value) {
const { cell } = this.props;
this._modeling.editCell(cell.businessObject, value);
}
-
- onFocus() {
- this.setState({
- isFocussed: true
- });
- }
-
-
- onBlur() {
- this.setState({
- isFocussed: false
- });
- }
-
-
render() {
const {
cell,
@@ -80,8 +56,6 @@ export default class DecisionRulesCellEditorComponent extends Component {
colIndex
} = this.props;
- const { isFocussed } = this.state;
-
const isUnaryTest = is(cell, 'dmn:UnaryTests');
const businessObject = cell.businessObject;
@@ -94,12 +68,7 @@ export default class DecisionRulesCellEditorComponent extends Component {
data-col-id={ col.id }
>
@@ -108,8 +77,47 @@ export default class DecisionRulesCellEditorComponent extends Component {
}
}
+class FeelEditor extends Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = { focussed: false };
-class TableCellEditor extends EditableComponent {
+ this.onFocus = this.onFocus.bind(this);
+ this.onBlur = this.onBlur.bind(this);
+ }
+
+ onFocus() {
+ this.setState({ focussed: true });
+ }
+
+ onBlur() {
+ this.setState({ focussed: false });
+ }
+
+ render() {
+ const { focussed } = this.state;
+ const className = `feel-editor${focussed ? ' focussed' : ''}`;
+
+ // TODO(@barmac): display only a single editor;
+ // required to workaround "replaceChild" error
+ return
+ { focussed &&
+
+ }
+ {} }
+ onFocus={ this.onFocus }
+ />
+
;
+ }
+}
+
+class TableCellEditor extends Component {
constructor(props, context) {
super(props, context);
@@ -164,10 +172,27 @@ class TableCellEditor extends EditableComponent {
return this._expressionLanguages.getDefault(elementType);
}
+ getEditor() {
+ return this.isFEEL() ? FeelEditor : ContentEditable;
+ }
+
+ isFEEL() {
+ return this.getExpressionLanguage() === 'feel';
+ }
+
+ getExpressionLanguage() {
+ const { businessObject } = this.props;
+
+ return businessObject.expressionLanguage ||
+ this.getDefaultExpressionLanguage(businessObject).value;
+ }
+
render() {
const {
businessObject,
- isFocussed
+ placeholder,
+ value,
+ onChange
} = this.props;
const description = this.getDescription(businessObject);
@@ -178,21 +203,23 @@ class TableCellEditor extends EditableComponent {
const isScript = this.isScript(businessObject);
+ const Editor = this.getEditor();
+
return (
-
+
{
isString(description)
- && !isFocussed
&&
}
+
{
- this.getEditor({
- className: isScript ? 'script-editor' : null
- })
- }
- {
- !isDefaultExpressionLanguage &&
- !isFocussed && (
+ !isDefaultExpressionLanguage && (
{
+ if (event.key === 'Enter') {
+ event.stopPropagation();
+ }
+ };
+
render() {
const {
@@ -54,7 +64,8 @@ export default class InputEditor extends Component {
const ExpressionEditor = this.getExpressionEditorComponent();
return (
-
+
{
+ requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+}
diff --git a/packages/dmn-js-decision-table/test/spec/features/decision-rules/DecisionRulesEditorSpec.js b/packages/dmn-js-decision-table/test/spec/features/decision-rules/DecisionRulesEditorSpec.js
index 2d9f772b6..c0e73a566 100644
--- a/packages/dmn-js-decision-table/test/spec/features/decision-rules/DecisionRulesEditorSpec.js
+++ b/packages/dmn-js-decision-table/test/spec/features/decision-rules/DecisionRulesEditorSpec.js
@@ -1,4 +1,4 @@
-import { bootstrapModeler, inject } from 'test/helper';
+import { bootstrapModeler, inject, act } from 'test/helper';
import { query as domQuery } from 'min-dom';
@@ -8,7 +8,6 @@ import { queryEditor } from 'dmn-js-shared/test/util/EditorUtil';
import TestContainer from 'mocha-test-container-support';
-import simpleXML from '../../simple.dmn';
import emptyRuleXML from './empty-rule.dmn';
import languageExpressionXML from '../../expression-language.dmn';
@@ -49,18 +48,55 @@ describe('features/decision-rules', function() {
describe('editing', function() {
- beforeEach(bootstrapModeler(simpleXML, {
+ beforeEach(bootstrapModeler(languageExpressionXML, {
modules: [
CoreModule,
ModelingModule,
DecisionRulesModule,
DecisionRulesEditorModule
],
- debounceInput: false
+ debounceInput: false,
+ expressionLanguages: {
+ options: CUSTOM_EXPRESSION_LANGUAGES
+ },
+ }));
+
+
+ it('should edit cell (FEEL)', inject(async function(elementRegistry) {
+
+ // given
+ const editor = queryEditor('[data-element-id="outputEntry2"]', testContainer);
+
+ await act(() => editor.focus());
+
+ // when
+ await changeInput(document.activeElement, 'foo');
+
+ // then
+ expect(elementRegistry.get('outputEntry2').businessObject.text).to.equal('foo');
+ }));
+
+
+ it('should edit cell - line breaks (FEEL)', inject(async function(elementRegistry) {
+
+ // given
+ let editor = queryEditor('[data-element-id="outputEntry2"]', testContainer);
+
+ await act(() => editor.focus());
+ editor = document.activeElement;
+
+ // when
+ await changeInput(editor, 'foo\nbar');
+
+ editor.blur();
+
+ // then
+ expect(elementRegistry.get('outputEntry2').businessObject.text)
+ .to.equal('foo\nbar');
}));
- it('should edit cell', inject(function(elementRegistry) {
+ it('should edit cell (non-FEEL)', inject(function(elementRegistry) {
// given
const editor = queryEditor('[data-element-id="inputEntry1"]', testContainer);
@@ -75,7 +111,7 @@ describe('features/decision-rules', function() {
}));
- it('should edit cell - line breaks', inject(function(elementRegistry) {
+ it('should edit cell - line breaks (non-FEEL)', inject(function(elementRegistry) {
// given
const editor = queryEditor('[data-element-id="inputEntry1"]', testContainer);
@@ -146,7 +182,8 @@ describe('features/decision-rules', function() {
editor.focus();
// then
- expect(domQuery('.dmn-expression-language', cell)).to.not.exist;
+ const badge = domQuery('.dmn-expression-language', cell);
+ expect(badge).to.satisfy(isNotDisplayed);
});
});
@@ -186,7 +223,8 @@ describe('features/decision-rules', function() {
editor.focus();
// then
- expect(domQuery('.dmn-expression-language', cell)).to.not.exist;
+ const badge = domQuery('.dmn-expression-language', cell);
+ expect(badge).to.satisfy(isNotDisplayed);
});
});
@@ -244,7 +282,8 @@ describe('features/decision-rules', function() {
editor.focus();
// then
- expect(domQuery('.dmn-expression-language', cell)).to.not.exist;
+ const badge = domQuery('.dmn-expression-language', cell);
+ expect(badge).to.satisfy(isNotDisplayed);
});
});
@@ -283,7 +322,8 @@ describe('features/decision-rules', function() {
editor.focus();
// then
- expect(domQuery('.dmn-expression-language', cell)).to.not.exist;
+ const badge = domQuery('.dmn-expression-language', cell);
+ expect(badge).to.satisfy(isNotDisplayed);
});
});
@@ -312,7 +352,7 @@ describe('features/decision-rules', function() {
const editor = queryEditor('[data-element-id="unaryTest_1"]', testContainer);
// then
- expect(editor.textContent).to.eql('-');
+ expect(editor.matches('[data-placeholder="-"]')).to.be.true;
}));
@@ -322,15 +362,27 @@ describe('features/decision-rules', function() {
const editor = queryEditor('[data-element-id="outputEntry_1"]', testContainer);
// then
- expect(editor.textContent).to.eql('');
+ expect(editor.matches('[data-placeholder="-"]')).to.be.false;
}));
});
});
-
// helpers //////////////////
+function isNotDisplayed(element) {
+ return !element || getComputedStyle(element).display === 'none';
+}
+
+/**
+ * @param {HTMLElement} input
+ * @param {string} value
+ */
+function changeInput(input, value) {
+ return act(() => {
+ input.textContent = value;
+ });
+}
function isFirefox() {
return /Firefox/.test(window.navigator.userAgent);
diff --git a/packages/dmn-js-shared/src/components/EditableComponent.js b/packages/dmn-js-shared/src/components/EditableComponent.js
index 22457da47..3a97ecaa9 100644
--- a/packages/dmn-js-shared/src/components/EditableComponent.js
+++ b/packages/dmn-js-shared/src/components/EditableComponent.js
@@ -171,7 +171,7 @@ export default class EditableComponent extends Component {
return (
{
+ handleMouseEvent = event => {
event.stopPropagation();
};
- handleKeyDown = (event) => {
+ handleKeyDownCapture = event => {
if (event.key === 'Enter') {
+ if (isAutocompleteOpen(this.node)) {
+ event.triggeredFromAutocomplete = true;
+ return;
+ }
+
+ // supress non cmd+enter newline
+ if (this.props.ctrlForNewline && !isCmd(event)) {
+ event.preventDefault();
+ }
+
+ if (this.props.singleLine) {
+ event.preventDefault();
+ }
+ }
+ };
+
+ /**
+ * @param {KeyboardEvent} event
+ */
+ handleKeyDown = event => {
+
+ // contain the event in the component to not trigger global handlers
+ if ([ 'Enter', 'Escape' ].includes(event.key) && event.triggeredFromAutocomplete) {
event.stopPropagation();
- event.preventDefault();
}
};
@@ -90,16 +125,27 @@ export default class LiteralExpression extends Component {
}
};
+ setNode = node => {
+ this.node = node;
+ };
+
render() {
return (
this.node = node }
+ ref={ this.setNode }
onClick={ this.handleMouseEvent }
- onKeyDown={ this.handleKeyDown }
onFocusIn={ this.props.onFocus }
onFocusOut={ this.props.onBlur }
/>
);
}
}
+
+function isCmd(event) {
+ return event.metaKey || event.ctrlKey;
+}
+
+function isAutocompleteOpen(node) {
+ return node.querySelector('.cm-tooltip-autocomplete');
+}
diff --git a/packages/dmn-js-shared/test/util/EditorUtil.js b/packages/dmn-js-shared/test/util/EditorUtil.js
index 07de4322e..fa39c4d8d 100644
--- a/packages/dmn-js-shared/test/util/EditorUtil.js
+++ b/packages/dmn-js-shared/test/util/EditorUtil.js
@@ -1,5 +1,5 @@
import { query as domQuery } from 'min-dom';
export function queryEditor(baseSelector, container) {
- return domQuery(baseSelector + ' .content-editable', container);
+ return domQuery(baseSelector + ' [contenteditable=true]', container);
}
\ No newline at end of file