Improve logic around swapping binding panel tabs
This commit is contained in:
parent
65ca394f61
commit
5b3280832c
|
@ -40,20 +40,20 @@
|
|||
indentMore,
|
||||
indentLess,
|
||||
} from "@codemirror/commands"
|
||||
import { Compartment } from "@codemirror/state"
|
||||
import { Compartment, EditorState } from "@codemirror/state"
|
||||
import { javascript } from "@codemirror/lang-javascript"
|
||||
import { EditorModes } from "./"
|
||||
import { themeStore } from "stores/portal"
|
||||
|
||||
export let label
|
||||
export let completions = []
|
||||
export let resize = "none"
|
||||
export let mode = EditorModes.Handlebars
|
||||
export let value = ""
|
||||
export let placeholder = null
|
||||
export let autocompleteEnabled = true
|
||||
export let autofocus = false
|
||||
export let jsBindingWrapping = true
|
||||
export let readonly = false
|
||||
|
||||
// Export a function to expose caret position
|
||||
export const getCaretPosition = () => {
|
||||
|
@ -143,32 +143,21 @@
|
|||
const buildBaseExtensions = () => {
|
||||
return [
|
||||
...(mode.name === "handlebars" ? [plugin] : []),
|
||||
history(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
highlightActiveLine(),
|
||||
syntaxHighlighting(oneDarkHighlightStyle, { fallback: true }),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
lineNumbers(),
|
||||
foldGutter(),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(v => {
|
||||
const docStr = v.state.doc?.toString()
|
||||
if (docStr === value) {
|
||||
return
|
||||
}
|
||||
dispatch("change", docStr)
|
||||
}),
|
||||
keymap.of(buildKeymap()),
|
||||
themeConfig.of([...(isDark ? [oneDark] : [])]),
|
||||
]
|
||||
}
|
||||
|
||||
// None of this is reactive, but it never has been, so we just assume most
|
||||
// config flags aren't changed at runtime
|
||||
const buildExtensions = base => {
|
||||
const complete = [...base]
|
||||
let complete = [...base]
|
||||
|
||||
if (autocompleteEnabled) {
|
||||
complete.push(
|
||||
|
@ -210,12 +199,36 @@
|
|||
|
||||
if (mode.name === "javascript") {
|
||||
complete.push(javascript())
|
||||
complete.push(highlightWhitespace())
|
||||
if (!readonly) {
|
||||
complete.push(highlightWhitespace())
|
||||
}
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
complete.push(placeholderFn(placeholder))
|
||||
}
|
||||
|
||||
if (readonly) {
|
||||
complete.push(EditorState.readOnly.of(true))
|
||||
} else {
|
||||
complete = [
|
||||
...complete,
|
||||
history(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
lineNumbers(),
|
||||
foldGutter(),
|
||||
keymap.of(buildKeymap()),
|
||||
EditorView.updateListener.of(v => {
|
||||
const docStr = v.state.doc?.toString()
|
||||
if (docStr === value) {
|
||||
return
|
||||
}
|
||||
dispatch("change", docStr)
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
return complete
|
||||
}
|
||||
|
||||
|
@ -301,7 +314,6 @@
|
|||
|
||||
/* Active line */
|
||||
.code-editor :global(.cm-line) {
|
||||
height: 16px;
|
||||
padding: 0 var(--spacing-s);
|
||||
color: var(--spectrum-alias-text-color);
|
||||
}
|
||||
|
@ -319,6 +331,9 @@
|
|||
background: var(--spectrum-global-color-gray-100) !important;
|
||||
z-index: -2;
|
||||
}
|
||||
.code-editor :global(.cm-highlightSpace:before) {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
|
||||
/* Code selection */
|
||||
.code-editor :global(.cm-selectionBackground) {
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
} from "../CodeEditor"
|
||||
import BindingSidePanel from "./BindingSidePanel.svelte"
|
||||
import EvaluationSidePanel from "./EvaluationSidePanel.svelte"
|
||||
import SnippetSidePanel from "./SnippetSidePanel.svelte"
|
||||
import { BindingHelpers } from "./utils"
|
||||
import formatHighlight from "json-format-highlight"
|
||||
import { capitalise } from "helpers"
|
||||
|
@ -38,6 +39,7 @@
|
|||
export let valid
|
||||
export let allowJS = false
|
||||
export let allowHelpers = true
|
||||
export let allowSnippets = true
|
||||
export let context = null
|
||||
export let autofocusEditor = false
|
||||
|
||||
|
@ -49,6 +51,7 @@
|
|||
const SidePanels = {
|
||||
Bindings: "FlashOn",
|
||||
Evaluation: "Play",
|
||||
Snippets: "Code",
|
||||
}
|
||||
|
||||
let initialValueJS = value?.startsWith?.("{{ js ")
|
||||
|
@ -64,10 +67,8 @@
|
|||
let evaluating = false
|
||||
|
||||
$: drawerContext?.modal.subscribe(val => (drawerIsModal = val))
|
||||
$: editorTabs = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
|
||||
$: sideTabs = context
|
||||
? [SidePanels.Evaluation, SidePanels.Bindings]
|
||||
: [SidePanels.Bindings]
|
||||
$: editorModeOptions = allowJS ? [Modes.Text, Modes.JavaScript] : [Modes.Text]
|
||||
$: sidePanelOptions = getSidePanelOptions(context, allowSnippets, mode)
|
||||
$: enrichedBindings = enrichBindings(bindings, context)
|
||||
$: usingJS = mode === Modes.JavaScript
|
||||
$: editorMode =
|
||||
|
@ -77,6 +78,22 @@
|
|||
$: runtimeExpression = readableToRuntimeBinding(enrichedBindings, value)
|
||||
$: requestUpdateEvaluation(runtimeExpression, context)
|
||||
$: bindingHelpers = new BindingHelpers(getCaretPosition, insertAtPos)
|
||||
$: {
|
||||
if (!sidePanelOptions.includes(sidePanel)) {
|
||||
sidePanel = SidePanels.Bindings
|
||||
}
|
||||
}
|
||||
|
||||
const getSidePanelOptions = (context, allowSnippets, mode) => {
|
||||
let options = [SidePanels.Bindings]
|
||||
if (context) {
|
||||
options.unshift(SidePanels.Evaluation)
|
||||
}
|
||||
if (allowSnippets && mode === Modes.JavaScript) {
|
||||
options.push(SidePanels.Snippets)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
const debouncedUpdateEvaluation = Utils.debounce((expression, context) => {
|
||||
expressionResult = processStringSync(expression || "", context)
|
||||
|
@ -135,11 +152,22 @@
|
|||
bindingHelpers.onSelectBinding(js ? jsValue : hbsValue, binding, { js })
|
||||
}
|
||||
|
||||
const onSelectSnippet = snippet => {
|
||||
bindingHelpers.onSelectSnippet(jsValue, snippet)
|
||||
}
|
||||
|
||||
const changeMode = newMode => {
|
||||
if (targetMode || newMode === mode) {
|
||||
return
|
||||
}
|
||||
if (editorValue) {
|
||||
|
||||
// Get the raw editor value to see if we are abandoning changes
|
||||
let rawValue = editorValue
|
||||
if (mode === Modes.JavaScript) {
|
||||
rawValue = decodeJSBinding(rawValue)
|
||||
}
|
||||
|
||||
if (rawValue?.length) {
|
||||
targetMode = newMode
|
||||
} else {
|
||||
mode = newMode
|
||||
|
@ -178,26 +206,26 @@
|
|||
<div class="main">
|
||||
<div class="tabs">
|
||||
<div class="editor-tabs">
|
||||
{#each editorTabs as tab}
|
||||
{#each editorModeOptions as editorMode}
|
||||
<ActionButton
|
||||
size="M"
|
||||
quiet
|
||||
selected={mode === tab}
|
||||
on:click={() => changeMode(tab)}
|
||||
selected={mode === editorMode}
|
||||
on:click={() => changeMode(editorMode)}
|
||||
>
|
||||
{capitalise(tab)}
|
||||
{capitalise(editorMode)}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="side-tabs">
|
||||
{#each sideTabs as tab}
|
||||
{#each sidePanelOptions as panel}
|
||||
<ActionButton
|
||||
size="M"
|
||||
quiet
|
||||
selected={sidePanel === tab}
|
||||
on:click={() => changeSidePanel(tab)}
|
||||
selected={sidePanel === panel}
|
||||
on:click={() => changeSidePanel(panel)}
|
||||
>
|
||||
<Icon name={tab} size="S" />
|
||||
<Icon name={panel} size="S" />
|
||||
</ActionButton>
|
||||
{/each}
|
||||
{#if drawerContext && get(drawerContext.resizable)}
|
||||
|
@ -287,6 +315,8 @@
|
|||
{evaluating}
|
||||
expression={editorValue}
|
||||
/>
|
||||
{:else if sidePanel === SidePanels.Snippets}
|
||||
<SnippetSidePanel addSnippet={onSelectSnippet} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script>
|
||||
import { Input, Layout, Icon, Popover } from "@budibase/bbui"
|
||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import { EditorModes } from "components/common/CodeEditor"
|
||||
|
||||
export let addSnippet
|
||||
export let snippets
|
||||
|
@ -15,16 +17,36 @@
|
|||
{
|
||||
name: "Square",
|
||||
code: `
|
||||
return function(num) {
|
||||
return num * num
|
||||
}
|
||||
`,
|
||||
return function(num) {
|
||||
return num * num
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "HelloWorld",
|
||||
code: `
|
||||
return "Hello, world!"
|
||||
`,
|
||||
return "Hello, world!"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "Colorful",
|
||||
code: `
|
||||
let a = null
|
||||
let b = "asdasd"
|
||||
let c = 123123
|
||||
let d = undefined
|
||||
let e = [1, 2, 3]
|
||||
let f = { foo: "bar" }
|
||||
let g = Math.round(1.234)
|
||||
if (a === b) {
|
||||
return c ?? e
|
||||
}
|
||||
return d || f
|
||||
// comment
|
||||
let h = 1 + 2 + 3 * 3
|
||||
let i = true
|
||||
let j = false
|
||||
`,
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -89,8 +111,15 @@
|
|||
on:mouseenter={stopHidingPopover}
|
||||
on:mouseleave={hidePopover}
|
||||
>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
<pre class="snippet-popover">{@html hoveredSnippet.code}</pre>
|
||||
<div class="snippet-popover">
|
||||
{#key hoveredSnippet}
|
||||
<CodeEditor
|
||||
value={hoveredSnippet.code.trim()}
|
||||
mode={EditorModes.JS}
|
||||
readonly
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<div class="snippet-side-panel">
|
||||
|
@ -114,15 +143,13 @@
|
|||
|
||||
<div class="snippet-list">
|
||||
{#each filteredSnippets as snippet}
|
||||
<div class="snippet">
|
||||
<div
|
||||
class="snippet-name"
|
||||
on:mouseenter={e => showSnippet(snippet, e.target)}
|
||||
on:mouseleave={hidePopover}
|
||||
on:click={() => addSnippet(snippet)}
|
||||
>
|
||||
{snippet.name}
|
||||
</div>
|
||||
<div
|
||||
class="snippet"
|
||||
on:mouseenter={e => showSnippet(snippet, e.target)}
|
||||
on:mouseleave={hidePopover}
|
||||
on:click={() => addSnippet(snippet)}
|
||||
>
|
||||
{snippet.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -155,14 +182,18 @@
|
|||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
.search-input,
|
||||
.title {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* List */
|
||||
.snippet-list {
|
||||
padding: 0 var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
.snippet {
|
||||
font-size: var(--font-size-s);
|
||||
|
@ -173,14 +204,14 @@
|
|||
border-color 130ms ease-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.snippet:hover {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Popover */
|
||||
.snippet-popover {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,4 +38,13 @@ export class BindingHelpers {
|
|||
this.insertAtPos({ start, end, value: insertVal })
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a JS/HBS helper to the expression
|
||||
onSelectSnippet(value, snippet) {
|
||||
const pos = this.getCaretPosition()
|
||||
const { start, end } = pos
|
||||
const jsVal = decodeJSBinding(value)
|
||||
const insertVal = jsInsert(jsVal, start, end, `snippets.${snippet.name}`)
|
||||
this.insertAtPos({ start, end, value: insertVal })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,22 +41,8 @@ const getContextValue = (path, context) => {
|
|||
return data
|
||||
}
|
||||
|
||||
const snippets = {
|
||||
Square: `
|
||||
return function(num) {
|
||||
return num * num
|
||||
}
|
||||
`,
|
||||
HelloWorld: `
|
||||
return "Hello, world!"
|
||||
`,
|
||||
}
|
||||
|
||||
// Evaluates JS code against a certain context
|
||||
module.exports.processJS = (handlebars, context) => {
|
||||
// for testing
|
||||
context.snippets = snippets
|
||||
|
||||
if (!isJSAllowed() || (isBackendService() && !runJS)) {
|
||||
throw new Error("JS disabled in environment.")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue