Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 74 additions & 8 deletions src/components/entries/FEEL/Feel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
useState
} from 'preact/hooks';
Expand Down Expand Up @@ -84,6 +85,7 @@ function FeelTextfield(props) {
} = props;

const [ localValue, setLocalValue ] = useState(getInitialFeelLocalValue(feel, value));
const localValueRef = useRef(localValue);

const editorRef = useShowEntryEvent(id);
const containerRef = useRef();
Expand All @@ -100,6 +102,12 @@ function FeelTextfield(props) {
const feelOnlyValue = getFeelValue(localValue);
const feelLanguageContext = useContext(FeelLanguageContext);

// keep state between rerenders
const isUnmountingRef = useRef(false);
const feelActiveRef = useRef(feelActive);
feelActiveRef.current = feelActive;


const [ focus, _setFocus ] = useState(undefined);

const {
Expand All @@ -121,6 +129,12 @@ function FeelTextfield(props) {
* @type { import('min-dash').DebouncedFunction }
*/
const handleInput = useDebounce(onInput, debounce);
const previousElementRef = useRef(element);

if (previousElementRef.current !== element) {
handleInput.flush?.();
previousElementRef.current = element;
}

const handleFeelToggle = useStaticCallback(() => {
if (feel === 'required') {
Expand All @@ -129,22 +143,38 @@ function FeelTextfield(props) {

if (!feelActive) {
setLocalValue('=' + localValue);
handleInput('=' + localValue);
handleInput.cancel?.();
onInput('=' + localValue);
} else {
setLocalValue(feelOnlyValue);
handleInput(feelOnlyValue);
handleInput.cancel?.();
onInput(feelOnlyValue);
}

// Prevent the FeelEditor unmount cleanup from double-committing
editorRef.current?.clearDirty?.();
});

const handleLocalInput = (newValue, useDebounce = true) => {

const currentLocalValue = localValueRef.current;

if (!useDebounce) {
handleInput.cancel?.();
}

if (feelActive) {
newValue = '=' + newValue;
}

if (newValue === localValue) {
if (newValue === currentLocalValue) {
if (!useDebounce) {
onInput(newValue);
}
return;
}

localValueRef.current = newValue;
setLocalValue(newValue);
if (useDebounce) {
handleInput(newValue);
Expand All @@ -164,7 +194,12 @@ function FeelTextfield(props) {
if (e.target.type === 'checkbox') {
onInput(e.target.checked);
} else {
const trimmedValue = e.target.value.trim();
let trimmedValue = trimValue(feel, e.target.value);

if (!trimmedValue && isString(localValueRef.current) && localValueRef.current.startsWith('=')) {
trimmedValue = localValueRef.current;
}

handleLocalInput(trimmedValue, false);
}

Expand Down Expand Up @@ -237,10 +272,29 @@ function FeelTextfield(props) {
}, [ value ]);

useEffect(() => {
localValueRef.current = localValue;
}, [ localValue ]);

// ensure values are committed during element switch
useEffect(() => {
return () => { isUnmountingRef.current = true; };
}, []);

useLayoutEffect(() => {
return () => {
if (!isUnmountingRef.current && feelActiveRef.current) {
handleInput.flush?.();
editorRef.current?.commit?.();
}
};
}, [ element, handleInput ]);

useEffect(() => {
return () => {
handleInput.flush?.();
eventBus.fire('propertiesPanel.closePopup');
};
}, []);
}, [ eventBus, handleInput ]);

// copy-paste integration
useEffect(() => {
Expand Down Expand Up @@ -271,10 +325,9 @@ function FeelTextfield(props) {

if (isFieldEmpty || isAllSelected) {
const textData = event.clipboardData.getData('text');
const trimmedValue = textData.trim();
const trimmedValue = trimValue(feel, textData);

setLocalValue(trimmedValue);
handleInput(trimmedValue);
handleLocalInput(trimmedValue, false);

if (!feelActive && isString(trimmedValue) && trimmedValue.startsWith('=')) {
setFocus(trimmedValue.length - 1);
Expand Down Expand Up @@ -906,3 +959,16 @@ function getInitialFeelLocalValue(feelType, value) {

return value;
}

/*
* trim value and also feel expression after the `=`
* ' = test ' -> '=test'
* ' hello ' -> 'hello'
*/
function trimValue(feelType, value) {
const trimmedValue = value.trim();
if (isFeelActive(feelType, trimmedValue)) {
return '=' + getFeelValue(trimmedValue).trim();
}
return value.trim();
}
101 changes: 87 additions & 14 deletions src/components/entries/FEEL/FeelEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@ const useBufferedFocus = function(editor, ref) {

const [ buffer, setBuffer ] = useState(undefined);

ref.current = useMemo(() => ({
focus: (offset) => {
if (editor) {
editor.focus(offset);
} else {
if (typeof offset === 'undefined') {
offset = Infinity;
ref.current = useMemo(() => {
const current = ref.current || {};

return {
...current,
focus: (offset) => {
if (editor) {
editor.focus(offset);
} else {
if (typeof offset === 'undefined') {
offset = Infinity;
}
setBuffer(offset);
}
setBuffer(offset);
}
}
}), [ editor ]);
};
}, [ editor ]);

useEffect(() => {
if (typeof buffer !== 'undefined' && editor) {
Expand All @@ -49,7 +54,7 @@ const FeelEditor = forwardRef((props, ref) => {
contentAttributes,
enableGutters,
value,
onInput,
onInput = noop,
onKeyDown: onKeyDownProp = noop,
onFeelToggle = noop,
onLint = noop,
Expand All @@ -64,7 +69,13 @@ const FeelEditor = forwardRef((props, ref) => {

const inputRef = useRef();
const [ editor, setEditor ] = useState();

const [ localValue, setLocalValue ] = useState(value || '');
const isDirtyRef = useRef(false);

const ignoreProgrammaticChangeRef = useRef(false);
const editorRef = useRef(null);
const blurHasCommittedRef = useRef(false);

const {
builtins,
Expand All @@ -74,11 +85,58 @@ const FeelEditor = forwardRef((props, ref) => {

useBufferedFocus(editor, ref);

const handleInput = useStaticCallback(newValue => {
onInput(newValue);
useEffect(() => {
if (!ref.current) {
return;
}

ref.current.commit = () => {
if (blurHasCommittedRef.current) {
blurHasCommittedRef.current = false;
return;
}

const stateValue = editor?._cmEditor?.state?.doc?.toString?.();
const domValue = editor?._cmEditor?.contentDOM?.textContent;
const currentValue = stateValue === '' && typeof domValue === 'string'
? domValue
: (stateValue || '');
isDirtyRef.current = false;
handleInput(currentValue, false);
};

ref.current.clearDirty = () => {
isDirtyRef.current = false;
};
}, [ editor ]);

const handleInput = useStaticCallback((newValue, useDebounce = true) => {
if (!ignoreProgrammaticChangeRef.current) {
isDirtyRef.current = useDebounce;
}

onInput(newValue, useDebounce);
setLocalValue(newValue);
});

// Commit immediately on blur
const handleBlur = useStaticCallback(() => {
if (!isDirtyRef.current) {
return;
}

const currentEditor = editorRef.current;
const stateValue = currentEditor?._cmEditor?.state?.doc?.toString?.();
const domValue = currentEditor?._cmEditor?.contentDOM?.textContent;
const currentValue = stateValue === '' && typeof domValue === 'string'
? domValue
: (stateValue || '');

blurHasCommittedRef.current = true;
isDirtyRef.current = false;
handleInput(currentValue, false);
});

useEffect(() => {

let editor;
Expand Down Expand Up @@ -119,16 +177,29 @@ const FeelEditor = forwardRef((props, ref) => {
parserDialect,
extensions: [
...enableGutters ? [ lineNumbers() ] : [],
EditorView.lineWrapping
EditorView.lineWrapping,
EditorView.domEventHandlers({ blur: handleBlur })
],
contentAttributes
});

editorRef.current = editor;
setEditor(
editor
);

return () => {
editorRef.current = null;
const stateValue = editor?._cmEditor?.state?.doc?.toString?.();
const domValue = editor?._cmEditor?.contentDOM?.textContent;

const currentValue = stateValue === '' && typeof domValue === 'string'
? domValue
: (stateValue || '');

if (isDirtyRef.current) {
handleInput(currentValue, false);
}
onLint([]);
inputRef.current.innerHTML = '';
setEditor(null);
Expand All @@ -144,7 +215,9 @@ const FeelEditor = forwardRef((props, ref) => {
return;
}

ignoreProgrammaticChangeRef.current = true;
editor.setValue(value);
ignoreProgrammaticChangeRef.current = false;
setLocalValue(value);
}, [ value ]);

Expand Down
Loading
Loading