2021-05-09 03:20:26 +00:00
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
2021-09-17 14:02:50 +00:00
|
|
|
import { useSpring, config, animated } from 'react-spring';
|
2021-12-10 03:09:23 +00:00
|
|
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|
|
|
|
import Tooltip from 'react-bootstrap/Tooltip';
|
2021-05-03 08:10:23 +00:00
|
|
|
import CodeMirror from '@uiw/react-codemirror';
|
2021-05-03 11:26:31 +00:00
|
|
|
import 'codemirror/mode/handlebars/handlebars';
|
2021-05-09 04:25:17 +00:00
|
|
|
import 'codemirror/mode/javascript/javascript';
|
2021-05-09 03:58:34 +00:00
|
|
|
import 'codemirror/mode/sql/sql';
|
2021-05-03 08:10:23 +00:00
|
|
|
import 'codemirror/addon/hint/show-hint';
|
2021-05-13 17:55:39 +00:00
|
|
|
import 'codemirror/addon/display/placeholder';
|
2021-05-03 08:10:23 +00:00
|
|
|
import 'codemirror/addon/search/match-highlighter';
|
2021-05-03 14:27:32 +00:00
|
|
|
import 'codemirror/addon/hint/show-hint.css';
|
2021-05-09 03:58:34 +00:00
|
|
|
import 'codemirror/theme/base16-light.css';
|
2021-09-15 15:40:59 +00:00
|
|
|
import 'codemirror/theme/duotone-light.css';
|
2021-07-03 17:07:50 +00:00
|
|
|
import 'codemirror/theme/monokai.css';
|
2021-05-03 16:04:54 +00:00
|
|
|
import { getSuggestionKeys, onBeforeChange, handleChange } from './utils';
|
2021-06-26 17:23:00 +00:00
|
|
|
import { resolveReferences } from '@/_helpers/utils';
|
2021-09-17 14:02:50 +00:00
|
|
|
import useHeight from '@/_hooks/use-height-transition';
|
2021-12-10 03:09:23 +00:00
|
|
|
import usePortal from '@/_hooks/use-portal';
|
2021-05-03 14:27:32 +00:00
|
|
|
|
2021-05-03 08:10:23 +00:00
|
|
|
export function CodeHinter({
|
2021-09-15 15:40:59 +00:00
|
|
|
initialValue,
|
|
|
|
|
onChange,
|
|
|
|
|
currentState,
|
|
|
|
|
mode,
|
|
|
|
|
theme,
|
|
|
|
|
lineNumbers,
|
|
|
|
|
placeholder,
|
|
|
|
|
ignoreBraces,
|
|
|
|
|
enablePreview,
|
2021-08-21 04:09:04 +00:00
|
|
|
height,
|
|
|
|
|
minHeight,
|
2021-09-15 15:40:59 +00:00
|
|
|
lineWrapping,
|
2021-12-10 03:09:23 +00:00
|
|
|
componentName = null,
|
|
|
|
|
usePortalEditor = true,
|
2021-12-25 03:21:19 +00:00
|
|
|
className = 'code-hinter',
|
2021-05-03 08:10:23 +00:00
|
|
|
}) {
|
2021-12-15 04:23:17 +00:00
|
|
|
const darkMode = localStorage.getItem('darkMode') === 'true';
|
2021-05-03 14:27:32 +00:00
|
|
|
const options = {
|
2021-12-10 03:09:23 +00:00
|
|
|
lineNumbers: lineNumbers ?? false,
|
|
|
|
|
lineWrapping: lineWrapping ?? true,
|
2021-05-03 14:27:32 +00:00
|
|
|
singleLine: true,
|
2021-05-09 03:58:34 +00:00
|
|
|
mode: mode || 'handlebars',
|
2021-05-03 14:27:32 +00:00
|
|
|
tabSize: 2,
|
2021-05-09 03:58:34 +00:00
|
|
|
theme: theme || 'default',
|
2021-05-03 14:27:32 +00:00
|
|
|
readOnly: false,
|
2021-05-13 17:55:39 +00:00
|
|
|
highlightSelectionMatches: true,
|
2021-09-15 15:40:59 +00:00
|
|
|
placeholder,
|
2021-05-03 08:10:23 +00:00
|
|
|
};
|
|
|
|
|
|
2021-05-09 03:20:26 +00:00
|
|
|
const [realState, setRealState] = useState(currentState);
|
2021-06-26 17:23:00 +00:00
|
|
|
const [currentValue, setCurrentValue] = useState(initialValue);
|
2021-09-15 15:40:59 +00:00
|
|
|
const [isFocused, setFocused] = useState(false);
|
2021-09-17 14:02:50 +00:00
|
|
|
const [heightRef, currentHeight] = useHeight();
|
|
|
|
|
const slideInStyles = useSpring({
|
|
|
|
|
config: { ...config.stiff },
|
|
|
|
|
from: { opacity: 0, height: 0 },
|
|
|
|
|
to: {
|
|
|
|
|
opacity: isFocused ? 1 : 0,
|
|
|
|
|
height: isFocused ? currentHeight : 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
2021-05-09 03:20:26 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
setRealState(currentState);
|
2021-09-21 13:48:28 +00:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2021-05-09 03:20:26 +00:00
|
|
|
}, [currentState.components]);
|
|
|
|
|
|
|
|
|
|
let suggestions = useMemo(() => {
|
2021-06-26 08:21:52 +00:00
|
|
|
return getSuggestionKeys(realState);
|
2021-09-21 13:48:28 +00:00
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2021-06-26 08:21:52 +00:00
|
|
|
}, [realState.components, realState.queries]);
|
2021-05-03 16:04:54 +00:00
|
|
|
|
2021-06-26 17:23:00 +00:00
|
|
|
function valueChanged(editor, onChange, suggestions, ignoreBraces) {
|
|
|
|
|
handleChange(editor, onChange, suggestions, ignoreBraces);
|
|
|
|
|
setCurrentValue(editor.getValue());
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-15 15:40:59 +00:00
|
|
|
const getPreviewContent = (content, type) => {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'object':
|
|
|
|
|
return JSON.stringify(content);
|
|
|
|
|
case 'boolean':
|
2021-09-16 12:01:22 +00:00
|
|
|
return content.toString();
|
2021-09-15 15:40:59 +00:00
|
|
|
default:
|
|
|
|
|
return content;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getPreview = () => {
|
2021-09-16 12:01:22 +00:00
|
|
|
const [preview, error] = resolveReferences(currentValue, realState, null, {}, true);
|
2021-12-15 04:23:17 +00:00
|
|
|
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
|
2021-09-16 12:01:22 +00:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
2021-12-15 04:23:17 +00:00
|
|
|
<animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
|
2021-09-17 14:02:50 +00:00
|
|
|
<div ref={heightRef} className="dynamic-variable-preview bg-red-lt px-1 py-1">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="heading my-1">
|
|
|
|
|
<span>Error</span>
|
|
|
|
|
</div>
|
|
|
|
|
{error.toString()}
|
2021-09-16 12:01:22 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2021-09-17 14:02:50 +00:00
|
|
|
</animated.div>
|
2021-09-16 12:01:22 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-15 15:40:59 +00:00
|
|
|
const previewType = typeof preview;
|
|
|
|
|
const content = getPreviewContent(preview, previewType);
|
|
|
|
|
|
|
|
|
|
return (
|
2021-12-15 04:23:17 +00:00
|
|
|
<animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
|
2021-09-17 14:02:50 +00:00
|
|
|
<div ref={heightRef} className="dynamic-variable-preview bg-green-lt px-1 py-1">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="heading my-1">
|
|
|
|
|
<span>{previewType}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{content}
|
2021-09-16 12:01:22 +00:00
|
|
|
</div>
|
2021-09-15 15:40:59 +00:00
|
|
|
</div>
|
2021-09-17 14:02:50 +00:00
|
|
|
</animated.div>
|
2021-09-15 15:40:59 +00:00
|
|
|
);
|
|
|
|
|
};
|
2021-11-15 06:18:09 +00:00
|
|
|
enablePreview = enablePreview ?? true;
|
2021-12-10 03:09:23 +00:00
|
|
|
|
|
|
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
const handleToggle = () => {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
setIsOpen(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const element = document.getElementsByClassName('portal-container');
|
|
|
|
|
if (element) {
|
|
|
|
|
const checkPortalExits = element[0]?.classList.contains(componentName);
|
|
|
|
|
|
|
|
|
|
if (checkPortalExits === false) {
|
|
|
|
|
const parent = element[0].parentNode;
|
|
|
|
|
parent.removeChild(element[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
}).then(() => {
|
|
|
|
|
setIsOpen(true);
|
|
|
|
|
forceUpdate();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
|
|
|
|
|
|
2021-12-25 03:21:19 +00:00
|
|
|
// const defaultClassName = isScrollable ? '' : 'code-hinter';
|
2021-05-03 14:27:32 +00:00
|
|
|
return (
|
2021-12-10 03:09:23 +00:00
|
|
|
<div className="code-hinter-wrapper" style={{ width: '100%' }}>
|
2021-09-17 14:02:50 +00:00
|
|
|
<div
|
2021-12-25 03:21:19 +00:00
|
|
|
className={`${className || 'codehinter-default-input'}`}
|
2021-09-17 14:02:50 +00:00
|
|
|
key={suggestions.length}
|
|
|
|
|
style={{ height: height || 'auto', minHeight, maxHeight: '320px', overflow: 'auto' }}
|
|
|
|
|
>
|
2021-12-15 04:23:17 +00:00
|
|
|
{usePortalEditor && <CodeHinter.PopupIcon callback={handleToggle} />}
|
2021-12-10 03:09:23 +00:00
|
|
|
<CodeHinter.Portal
|
|
|
|
|
isOpen={isOpen}
|
|
|
|
|
callback={setIsOpen}
|
|
|
|
|
componentName={componentName}
|
|
|
|
|
key={suggestions.length}
|
|
|
|
|
customComponent={getPreview}
|
|
|
|
|
forceUpdate={forceUpdate}
|
|
|
|
|
optionalProps={{ height: 300 }}
|
2021-12-15 04:23:17 +00:00
|
|
|
darkMode={darkMode}
|
|
|
|
|
selectors={{ className: 'preview-block-portal' }}
|
2021-12-10 03:09:23 +00:00
|
|
|
>
|
|
|
|
|
<CodeMirror
|
|
|
|
|
value={initialValue}
|
|
|
|
|
realState={realState}
|
|
|
|
|
scrollbarStyle={null}
|
|
|
|
|
height={height || 'auto'}
|
|
|
|
|
onFocus={() => setFocused(true)}
|
|
|
|
|
onBlur={(editor) => {
|
|
|
|
|
const value = editor.getValue();
|
|
|
|
|
onChange(value);
|
|
|
|
|
setFocused(false);
|
|
|
|
|
}}
|
|
|
|
|
onChange={(editor) => valueChanged(editor, onChange, suggestions, ignoreBraces)}
|
|
|
|
|
onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)}
|
|
|
|
|
options={options}
|
|
|
|
|
viewportMargin={Infinity}
|
|
|
|
|
/>
|
|
|
|
|
</CodeHinter.Portal>
|
2021-09-17 14:02:50 +00:00
|
|
|
</div>
|
2021-12-10 03:09:23 +00:00
|
|
|
{enablePreview && !isOpen && getPreview()}
|
2021-11-15 06:18:09 +00:00
|
|
|
</div>
|
2021-05-03 14:27:32 +00:00
|
|
|
);
|
2021-05-09 03:20:26 +00:00
|
|
|
}
|
2021-12-10 03:09:23 +00:00
|
|
|
|
|
|
|
|
const PopupIcon = ({ callback }) => {
|
|
|
|
|
return (
|
|
|
|
|
<div className="d-flex justify-content-end" style={{ position: 'relative' }}>
|
|
|
|
|
<OverlayTrigger
|
|
|
|
|
trigger={['hover', 'focus']}
|
|
|
|
|
placement="top"
|
|
|
|
|
delay={{ show: 800, hide: 100 }}
|
|
|
|
|
overlay={<Tooltip id="button-tooltip">{'Pop out code editor into a new window'}</Tooltip>}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
className="svg-icon m-2 popup-btn"
|
|
|
|
|
src="/assets/images/icons/portal-open.svg"
|
|
|
|
|
width="12"
|
|
|
|
|
height="12"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
callback();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</OverlayTrigger>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const Portal = ({ children, ...restProps }) => {
|
|
|
|
|
const renderPortal = usePortal({ children, ...restProps });
|
|
|
|
|
|
|
|
|
|
return <React.Fragment>{renderPortal}</React.Fragment>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
CodeHinter.PopupIcon = PopupIcon;
|
|
|
|
|
CodeHinter.Portal = Portal;
|